/*
 * writefil.c - Pcal routines concerned with writing the PostScript output
 *
 * Contents:
 *
 *		def_footstring
 *		expand_fmt
 *		find_daytext
 *		find_noteboxes
 *		print_colors
 *		print_dates
 *		print_db_word
 *		print_julian_info
 *		print_month
 *		print_moon_info
 *		print_pstext
 *		print_text
 *		print_word
 *		select_color
 *		set_rgb
 *		write_calfile
 *		write_psfile
 *
 * Revision history:
 *
 *	4.5	AWR	04/05/94	select real vs. dummy PostScript code
 *					within write_psfile() (cf. pcalinit.ps)
 *
 *			11/30/93	pre-scale all fonts used (as suggested
 *					by Andrew Houghton; cf. pcalinit.ps)
 *
 *			11/24/93	replace find_holidays() with
 *					print_colors() (cf. drawnums{} in
 *					pcalinit.ps)
 *
 *			11/16/93	Add set_rgb() to handle red:green:blue
 *					values for date/fill colors
 *
 *			09/23/93	Support both ROMAN-8 and LATIN-8 font
 *					mappings
 *
 *		AWR	07/09/93	Revised PostScript comment block
 *
 *		AWR	03/01/93	add optional mapping of 8-bit fonts
 *
 *		AWR	02/05/93	Support -# flag (multiple copies of
 *					each output page)
 *
 *		AWR	04/22/92	use STRSIZ for temp buffer size
 *
 *	4.4	AWR	04/07/92	revise to use new PUTCHAR and PUTSTR
 *					macros (cf. pcaldefs.h)
 *
 *			01/20/92	support -z and revised -[bgGO] flags
 *
 *		AWR	01/13/92	support optional font size in -d and
 *					-t flags; move initialization of fonts
 *					and sizes here (from pcalinit.ps)
 *
 *	4.3	AWR	12/03/91	add support for -s flag (specify
 *					alternate date/fill box shading values)
 *
 *	4.2	AWR	10/08/91	add support for -[kK] flags (change
 *					position of small calendars)
 *
 *			10/03/91	add find_noteboxes(); revise to print
 *					text in multiple notes boxes
 *
 *					add support for -S flag
 *
 *			10/02/91	modify def_footstring() to handle all
 *					types of strings; use it to print notes
 *					header (-N flag)
 *
 *			09/19/91	add write_calfile(), print_dates(),
 *					and new print_text() to generate 
 *					input for Un*x "calendar" utility;
 *					renamed old print_text() as
 *					print_pstext() for clarity; revised
 *					to simplify setting working date
 *
 *	4.11	AWR	08/23/91	revise expand_fmt() to write results
 *					to string instead of stdout; revise
 *					print_word() to avoid writing null
 *					strings
 *
 *		AWR	08/21/91	use ABBR_DAY_LEN and ABBR_MONTH_LEN
 *					(cf. pcallang.h) to print abbreviated
 *					day/month names
 *
 *		AWR	08/21/91	add %u and %w (calculate week number
 *					so that 1/1 is always week 1); support
 *					%[+-]<n>[DWMY] to adjust working date
 *					by +|- <n> days/weeks/months/years
 *
 *	4.1	AWR	08/16/91	Support -G flag (outlined gray dates)
 *
 *	4.02	AWR	07/02/91	Added "%" expansions in text strings
 *					(cf. expand_fmt())
 *
 *	4.0	AWR	01/28/91	Support -B, -w flags and moon file
 *
 *			01/15/91	Extracted from pcal.c
 *
 */

/*
 * Standard headers:
 */

#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <time.h>

/*
 * Pcal-specific definitions:
 */

#include "pcaldefs.h"
#include "pcalglob.h"
#define  WRITEFIL		/* to get ordinal_suffix() from pcallang.h */
#include "pcallang.h"
#include "pcalinit.h"		/* PostScript boilerplate */

/*
 * Macros:
 */

/* make sure PRT() doesn't round "ph" up to 1.0 when printing it */
#define PRT_TWEAK(ph)		((ph) >= 0.9995 ? 0.0 : (ph))

/* advance working date by n days */
#define SET_DATE(n)	do {						\
	MAKE_DATE(date, work_month, work_day + (n), work_year);		\
	normalize(&date);						\
	work_month = date.mm, work_day = date.dd, work_year = date.yy;	\
} while (0)

/* prescale a font and add its name to list */
#define ADDFONT(name, size, font, isarray)	do {			\
	char *p = alloc(strlen(name) + strlen(size) + 2);		\
	sprintf(allfonts[nfonts++] = p, "%s_%s", name, size);		\
	PRT("/%s { %sfontsize ", p, font);				\
	if (isarray) PRT("%s get ", size);				\
	PRT("%sfont FontFind } def\n", font);				\
} while (0)

#define GEN_PSCODE(arr)							\
	for (ap = arr; *ap; ap++)					\
		PRT("%s\n", *ap)

#define FOOTSTRINGS()	(lfoot[0] || cfoot[0] || rfoot[0])

/* various PostScript comments and commands for start/end of page */

#ifdef EPS

/* macro definitions for generating EPS output */

#define PS_STARTPAGE()	do {						\
	page++;								\
	PRT("%%%%Page: %d %d\n", page, page);				\
	PRT("%%%%BeginPageSetup\n");					\
	PRT("clear flush\n");						\
	PRT("/PageNum { %d } def\n", page);				\
	PRT("/PageState save def\n");					\
	PRT("%%%%EndPageSetup\n");					\
} while (0)

#define PS_ENDPAGE()	do {						\
	PRT("%%%%PageTrailer\n");					\
	PRT("showpage\n");						\
	PRT("clear flush\n");						\
	PRT("PageState restore\n");					\
} while (0)

#define PS_TRAILER()	do {						\
	PRT("%%%%Trailer\n");						\
	PRT("clear flush\n");						\
	PRT("%%%%EOF\n");						\
} while (0)

#else /* non-EPS flavors of above macros */
#define PS_STARTPAGE()
#define PS_ENDPAGE()		PRT("showpage\n");
#define PS_TRAILER()
#endif

/* reset working date to original date */
#define RESET_DATE()	\
work_month = this_month, work_day = this_day, work_year = this_year

#define NEWFONT		"-8"	/* suffix for new 8-bit fonts */

#define MAXFONT		20	/* maximum number of fonts to prescale */

/*
 * Globals:
 */

/* order of following strings must conform to #define's in pcaldefs.h (q.v.) */
static char *cond[3] = {"false", "true", "(some)"};

static int this_day, this_month, this_year;	/* current day */
static int work_day, work_month, work_year;	/* working day (cf. expand_fmt()) */
static char *kw_note, *kw_opt, *kw_year;	/* keywords for -c output */

static int debug_text;				/* generate debug output */


/*
 * write_psfile - write PostScript code
 *
 * The actual output of the PostScript code is straightforward.  This routine
 * writes a PostScript header followed by declarations of all the PostScript
 * variables affected by command-line flags and/or language dependencies.  It
 * the generates the PostScript boilerplate generated from pcalinit.ps, and
 * finally calls print_month() to generate the PostScript code for each
 * requested month.
 *
 */
void
#ifdef PROTOS
write_psfile(int month,			/* starting month   */
	     int year,			/* starting year    */
	     int nmonths)		/* number of months */
#else
write_psfile(month, year, nmonths)
	int month;			/* starting month   */
	int year;			/* starting year    */
	int nmonths;			/* number of months */
#endif
{
	int i, nfonts, nfsize, copies, dfltsize, color_dates;
	char *p, **ap, tmp[STRSIZ], alt_color, *allfonts[MAXFONT];
	struct tm *p_tm;
	time_t t;
#ifdef UN_X
	struct passwd *pw;
#endif
	/* default date, title, weekday font sizes (small/medium/large) */
	static int dsize[3] = DATEFONTSIZE;
	static int tsize[3] = TITLEFONTSIZE;
	static int wsize[3] = WEEKDAYFONTSIZE;
	static int fsize[3] = FOOTFONTSIZE;
	static int dmargin[3] = DATEMARGIN;
	static double gwidth[3] = GRIDLINEWIDTH;
	static char *calsize[3] = CALSIZE;

	debug_text = DEBUG(DEBUG_TEXT);		/* debug text output? */

	/*
	 * Write out PostScript prolog (including version/datefile stamp)
	 */

	/* comment block at top */

 	PRT("%%!%s\n", PS_RELEASE);		/* PostScript release */


	PRT("%%%%CreationDate: ");		/* date/time stamp */
	p_tm = localtime((time(&t), &t));
	if (date_style == EUR_DATES)
		PRT("%d.%d.", p_tm->tm_mday, p_tm->tm_mon + 1);
	else
		PRT("%d/%d/", p_tm->tm_mon + 1, p_tm->tm_mday);
	PRT("%02d %02d:%02d:%02d\n", p_tm->tm_year % 100, p_tm->tm_hour,
		p_tm->tm_min, p_tm->tm_sec);
	PRT("%%%%Creator: %s %s %s\n", CREATED_MSG, progname, version);

#ifdef UN_X
	if ((pw = getpwuid(getuid())) != NULL) {
		strcpy(tmp, pw->pw_gecos);
		if ((p = strchr(tmp, ',')) != NULL)
			*p = '\0';
		PRT("%%%%For: %s\n", pw->pw_name);
		PRT("%%%%Routing: %s\n", tmp);
	}
#endif

	/* Identify the output (month/year range and input file) */

	if (do_whole_year && month == JAN) {
		PRT("%%%%Title: %s %d", TITLE_MSG, year);
		if (nmonths > 12)
			PRT(" - %d", year + ((nmonths - 1) / 12));
	} else {
		char c = date_style == EUR_DATES ? '.' : '/';
		PRT("%%%%Title: %s %d%c%02d", TITLE_MSG, month, c, year % 100);
		if (nmonths > 1)
			PRT(" - %d%c%02d", (month + (nmonths - 1) - 1) % 12 + 1,
			    c, (year + (nmonths - 1 + month - 1) / 12) % 100);
	}
	if (*datefile)
		PRT(DATEFILE_MSG, datefile);
	PRT("\n");

	/* Miscellaneous other identification */

	PRT("%%%%Pages: %d\n", do_whole_year ? nmonths / 12 : nmonths);
	PRT("%%%%PageOrder: Ascend\n");
	PRT("%%%%Orientation: %s\n", rotate == LANDSCAPE ? "Landscape" :
							   "Portrait");
	PRT("%%%%BoundingBox: 0 0 612 792\n");
	PRT("%%%%ProofMode: NotifyMe\n");
	PRT("%%%%EndComments\n\n");

	/* number of copies (from -#<n> flag) */

	if ((copies = atoi(ncopy)) > MAXCOPY)
		copies = MAXCOPY;		/* save a tree */
	if (copies > 1)
		PRT("/#copies %d def\n", copies);

	/* calendar sizes: to minimize number of pre-scaled fonts, whole-
	 * year calendars define 'medium' as 0 and the other sizes as -1
	 * (not used); single-month calendars define 'large' as 0, 'small'
	 * as 1, and 'medium' as -1 (not used)
	 */

	for (i = SMALL; i <= LARGE; i++)
		PRT("/%s %d def\n", calsize[i],
			do_whole_year ? (i == MEDIUM ? 0 : -1) :
					(i == MEDIUM ? -1 : i == SMALL));

	/* font names and sizes */

	/* v4.4 supports user override of note and large date/title sizes */
	nfsize       = (p = strrchr(notesfont, '/')) ? *p++ = '\0', atoi(p) :
				atoi(strrchr(NOTESFONT, '/') + 1);
	dsize[LARGE] = (p = strrchr(datefont, '/')) ? *p++ = '\0', atoi(p) :
				atoi(strrchr(DATEFONT, '/') + 1);
	tsize[LARGE] = (p = strrchr(titlefont, '/')) ? *p++ = '\0', atoi(p) :
				atoi(strrchr(TITLEFONT, '/') + 1);

	/* enlarge footer strings in whole-year/portrait mode */
	if (do_whole_year && rotate == PORTRAIT)
		fsize[MEDIUM] *= 1.25;

	/*
	 * if 8-bit remapping has been requested (-r flag), create new fonts
	 * with desired character remapping (Roman8 or Latin1, as requested)
	 */

	if (mapfonts != NOMAP) {

		/* include desired mapping */
		GEN_PSCODE(mapfonts == ROMAN8 ? ps_roman8 : ps_latin1);

		/* boilerplate to remap for 8-bit fonts */
		GEN_PSCODE(ps_remap);

		/* always generate code to remap title font */
		PRT("/%s /%s%s remap_font\n", titlefont, titlefont, NEWFONT);
		strcat(titlefont, NEWFONT);

#if MAP_DATEFONT	/* any text printed in date font (cf. pcaldefs.h)? */
		/* generate code to remap date font if necessary */
		PRT("FontDirectory /%s%s known not {\n", datefont, NEWFONT);
		PRT("/%s /%s%s remap_font\n", datefont, datefont, NEWFONT);
		PRT("} if\n");
		strcat(datefont, NEWFONT);
#endif

		/* generate code to remap notes font if necessary */
		PRT("FontDirectory /%s%s known not {\n", notesfont, NEWFONT);
		PRT("/%s /%s%s remap_font\n", notesfont, notesfont, NEWFONT);
		PRT("} if\n");
		strcat(notesfont, NEWFONT);
	}

	/* define title, notes, and date fonts */
	PRT("/titlefont /%s def\n", titlefont);
	PRT("/datefont /%s def\n", datefont);
	PRT("/notesfont /%s def\n", notesfont);

	/* typically defined in terms of above fonts - must define last */
	PRT("/weekdayfont %s def\n", WEEKDAYFONT);
	PRT("/footfont %s def\n", FOOTFONT);
	PRT("/headingfont %s def\n", HEADINGFONT);

	/* print various font sizes and line/margin widths as PostScript arrays:
	 * one element for whole-year calendars; two (large, small) for single-
	 * month calendars
	 */

	if (do_whole_year) {
		PRT("/datemargin [ %d ] def\n", dmargin[MEDIUM]);
		PRT("/gridlinewidth [ %.1f ] def\n", gwidth[MEDIUM]);
		PRT("/titlefontsize [ %d ] def\n", tsize[MEDIUM]);
		PRT("/datefontsize [ %d ] def\n", dsize[MEDIUM]);
	} else {
		PRT("/datemargin [ %d %d ] def\n", dmargin[LARGE], dmargin[SMALL]);
		PRT("/gridlinewidth [ %.1f %.1f ] def\n", gwidth[LARGE], gwidth[SMALL]);
		PRT("/titlefontsize [ %d %d ] def\n", tsize[LARGE], tsize[SMALL]);
		PRT("/datefontsize [ %d %d ] def\n", dsize[LARGE], dsize[SMALL]);
	}

	dfltsize = do_whole_year ? MEDIUM : LARGE;

	PRT("/weekdayfontsize %d def\n", wsize[dfltsize]);
	PRT("/footfontsize %d def\n", fsize[dfltsize]);
	PRT("/notesfontsize %d def\n", nfsize);
	PRT("/headingfontsize %d def\n", HEADINGFONTSIZE);

	/* pre-scale all fonts used by PostScript code; try to be smart about
	 * skipping those that we know (at this point) won't be needed (whole-
	 * year calendars use either 3 or 4 fonts, while single-month calendars 
	 * can use anywhere from 3 to 8).  "FF" et. al. are indices into the
	 * font array (cf pcalinit.ps) for the different font types.
	 */

	PRT("/FontFind { findfont exch scalefont } def\n");

	nfonts = 0;

	PRT("/FF %d def\n", nfonts);			/* footers */
	if (FOOTSTRINGS())
		ADDFONT("ff", calsize[dfltsize], "foot", FALSE);

	if (do_whole_year) {
		PRT("/TF %d def\n", nfonts);		/* month/year title */
		ADDFONT("tf", calsize[MEDIUM], "title", TRUE);

		PRT("/DF %d def\n", nfonts);		/* dates */
		ADDFONT("df", calsize[MEDIUM], "date", TRUE);
	} else {
		PRT("/HF %d def\n", nfonts);		/* 'Notes' heading */
		if (notes_hdr[0])
			ADDFONT("hf", calsize[LARGE], "heading", FALSE);

		/* large/small (if used) scalings of the same font must be
		 * contiguous and appear in that order
		 */

		PRT("/TF %d def\n", nfonts);		/* large/small title */
		ADDFONT("tf", calsize[LARGE], "title", TRUE);
		if (small_cal_pos != SC_NONE)
			ADDFONT("tf", calsize[SMALL], "title", TRUE);

		PRT("/DF %d def\n", nfonts);		/* large/small dates */
		ADDFONT("df", calsize[LARGE], "date", TRUE);
		if (small_cal_pos != SC_NONE)
			ADDFONT("df", calsize[SMALL], "date", TRUE);

		PRT("/NF %d def\n", nfonts);		/* text in boxes */
		ADDFONT("nf", calsize[LARGE], "notes", FALSE);
	}

	PRT("/WF %d def\n", nfonts);			/* weekdays */
	ADDFONT("wf", calsize[dfltsize], "weekday", FALSE);

	/* generate the font array (automatically in sync with above) */

	PRT("/allfonts [\n\t");
	for (i = 0; i < nfonts; i++) {
		PRT("%s ", allfonts[i]);
		free(allfonts[i]);
	}
	PRT("\n] def\n");

	/*
	 * define various strings and numeric values used by Pcal
	 */

	/* month names */

	PRT("/month_names [");
	for (i = JAN; i <= DEC; i++) {
		PRT(i % 6 == 1 ? "\n\t" : " ");
		(void) print_word(months[i-1]);
	}
	PRT(" ] def\n");

	/* day names - abbreviate if printing entire year on page */

	PRT("/day_names [");
	for (i = SUN; i <= SAT; i++) {
		PRT(i % 6 == 0 && ! do_whole_year ? "\n\t" : " ");
		strcpy(tmp, days[(i + first_day_of_week) % 7]);
		if (do_whole_year)
			tmp[ABBR_DAY_LEN] = '\0';
		(void) print_word(tmp);
		}
	PRT(" ] def\n");

	/* line separator */

	PRT("/linesep ");
	print_word(LINE_SEP);
	PRT(" def\n");

 	/* rotation, scale, and translate values */
 
 	PRT("/rval %d def\n", rotate);
 	PRT("/xsval %s def\n/ysval %s def\n", xsval, ysval);
 	PRT("/xtval %s def\n/ytval %s def\n", xtval, ytval);

	/* moon, Julian date, and box fill flags */

	PRT("/draw-moons %s def\n", cond[draw_moons]);
	PRT("/julian-dates %s def\n", cond[julian_dates]);
	PRT("/fill-boxes %s def\n", cond[! blank_boxes]);

	/* position of small calendars */

	PRT("/prev_small_cal %d def\n", prev_cal_box[small_cal_pos]);
	PRT("/next_small_cal %d def\n", next_cal_box[small_cal_pos]);

	/* date and fill box shading values */

	strcpy(tmp, shading);
	*(p = strchr(tmp, '/')) = '\0';
	PRT("/dategray %s def\n", set_rgb(tmp));
	PRT("/fillgray %s def\n", set_rgb(p + 1));
	color_dates = strchr(tmp, RGB_CHAR) != NULL;

	/* PostScript boilerplate (part 1 of 1) */

	GEN_PSCODE(header);

	/* Additional PostScript code tailored to this calendar */

	GEN_PSCODE(color_dates ?
			ps_prtday_rgb :		/* color prtday{} */
			ps_prtday_bw);		/* B&W prtday{} */

	GEN_PSCODE(FOOTSTRINGS() ?
			ps_footer :		/* at least one foot string */
			ps_nofooter);		/* no foot strings */

	GEN_PSCODE(blank_boxes ?
			ps_nofill :		/* blank fill boxes */
			ps_fill);		/* shaded fill boxes */

	if (do_whole_year) {
		GEN_PSCODE(rotate == LANDSCAPE ?
			   ps_year_l :		/* medium months (landscape) */
			   ps_year_p);		/* medium months (portrait)  */
		GEN_PSCODE(ps_nojulians);	/* no julians or moons */
		GEN_PSCODE(ps_nomoons);
	} else {
		GEN_PSCODE(julian_dates == NO_JULIANS ?
				ps_nojulians :		/* no julian dates */
				ps_julians);		/* some julian dates */
		GEN_PSCODE(draw_moons == NO_MOONS ?
				ps_nomoons :		/* no moons */
				ps_moons);		/* some or all moons */
		if (head)
			GEN_PSCODE(ps_text);		/* date text */
		GEN_PSCODE(ps_month);			/* single month */
	}

	/*
	 * Write out PostScript code to print calendars
	 */

	for (this_month = month, this_year = year; nmonths--; ) {
		print_month(this_month, this_year);
		this_year = NEXT_YEAR(this_month, this_year);
		this_month = NEXT_MONTH(this_month, this_year);
	}

	/* generate trailer at end of PostScript output */
	PS_TRAILER();
}


/*
 * write_calfile - write dates in format suitable for Un*x "calendar" utility
 * (and subsequent use by Pcal)
 */
void
#ifdef PROTOS
write_calfile(int month,		/* starting month   */
	      int year,			/* starting year    */
	      int nmonths)		/* number of months */
#else
write_calfile(month, year, nmonths)
	int month			/* starting month   */;
	int year;			/* starting year    */
	int nmonths;			/* number of months */
#endif
{
	KWD *k;

	/* look up the Pcal keywords (assumed present) for the -c output file */
	for (k = keywds; k->name; k++) {
		if (k->code == DT_NOTE) kw_note = k->name;
		if (k->code == DT_OPT)  kw_opt  = k->name;
		if (k->code == DT_YEAR) kw_year = k->name;
	}

	/* print the date style for subsequent use by Pcal */
	PRT("%s -%c\n", kw_opt, date_style == USA_DATES ? F_USA_DATES :
							  F_EUR_DATES);

	for (this_month = month, this_year = year; nmonths--; ) {
		print_dates(this_month, this_year);
		this_year = NEXT_YEAR(this_month, this_year);
		this_month = NEXT_MONTH(this_month, this_year);
	}
}

/*
 * low-level utilities for PostScript generation
 */

/*
 * set_rgb - convert "<r>:<g>:<b>" to [r g b] or "<gray>" to [gray];
 * return pointer to static buffer containing converted string
 */
char *
#ifdef PROTOS
set_rgb(char *s)
#else
set_rgb(s)
	char *s;
#endif
{
	static char buf[STRSIZ];
	char *p1, *p2;
	double val[3];
	int n;

	val[0] = val[1] = val[2] = 0;		/* defaults */

	/* extract 1 - 3 floating-point values from string */
	for (n = 1, p1 = s; n <= 3; n++, p1 = p2 + 1) {
		val[n-1] = atof(p1);
		if ((p2 = strchr(p1, RGB_CHAR)) == NULL)
			break;
	}

	/* single value is gray scale; assume anything else is [r g b] */
	if (n > 1)
		sprintf(buf, "[%.3f %.3f %.3f]", val[0], val[1], val[2]);
	else
		sprintf(buf, "[%.3f]", val[0]);

	return buf; 
}

/*
 * select_color - if the holiday color has not been explicitly selected,
 * choose a color which contrasts with the majority of weekday colors
 */
int
#ifdef PROTOS
select_color(void)
#else
select_color()
#endif
{
	int i, min, index;
	char count[NUM_COLORS];

	for (i = 0; i < NUM_COLORS; i++)		/* clear counts */
		count[i] = 0;

	for (i = SUN; i <= SAT; i++)			/* count colors */
		count[day_color[i]]++;

	/* find smallest non-zero count; set its index and value */
	for (i = 0, min = 99; i < NUM_COLORS; i++)
		if (count[i] && count[i] < min)
			min = count[index = i];

	/* return least-used color (or pick one if only one color used) */
	return min == 7 ? index == BLACK ? GRAY : BLACK : index;
}


/*
 * expand_fmt - expand a strftime-like date format specifier; pcal supports
 * %[aAbBdjmUWyY] from strftime() plus %[luwDM] and prefixes [0o+-] (see
 * below); places expanded string in output buffer and returns pointer to
 * character following end of format specifier.  Assumes working date has
 * been initialized (via RESET_DATE() macro) prior to first call for a given
 * text string
 */
char *
#ifdef PROTOS
expand_fmt(char *buf,		/* output buffer (filled in)	    */
	   char *p)		/* character following percent sign */
#else
expand_fmt(buf, p)
	char *buf;		/* output buffer (filled in)	    */
	char *p;		/* character following percent sign */
#endif
{
	char c;
	static char *prefixes = "0o+-";
	int firstday, wkday;
	int adjust = 0, print_lz = FALSE, ordinal = FALSE, prev_num = -1;
	int num_present = FALSE, num_value = 0;
	DATE date;

	/* For compatibility with version 4.1, still support %[+-][bBdmY]
	 * (print the next/last month-name/day/month/year).  Version 4.11
	 * introduces %[+-]<n>[DWMY], which adjusts the working date by
	 * [+-]<n> days/weeks/months/years; this method is preferred due
	 * to its greater flexibility.
	 */

	buf[0] = '\0';		/* initialize output to null string */
	
	do {			/* loop until format character found */
		switch (c = *p++) {
		case 'a':	/* %a : abbreviated weekday */
		case 'A':	/* %A : full weekday */
			wkday = calc_weekday(work_month, work_day, work_year);
			strcpy(buf, days[wkday]);
			if (c == 'a')
				buf[ABBR_DAY_LEN] = '\0';
			break;

		case 'b':	/* %b : abbreviated month name */
		case 'B':	/* %B : full month name */
			strcpy(buf, months[(work_month + adjust + 11) % 12]);
			if (c == 'b')
				buf[ABBR_MONTH_LEN] = '\0';
			break;

		case 'd':	/* %d : day of month (01-31) */
			prev_num = work_day;
			sprintf(buf, print_lz ? "%02d" : "%d", prev_num);
			break;

		case 'D':	/* %D : adjust working date by <N> days (NEW) */
			if (!num_present || num_value == 0)
				RESET_DATE();
			else
				SET_DATE(adjust * num_value);
			break;

		case 'j':	/* %j : day of year (001-366) */
			prev_num = DAY_OF_YEAR(work_month, work_day,
					work_year);
			sprintf(buf, print_lz ? "%03d" : "%d", prev_num);
			break;

		case 'l':	/* %l : days left in year (000-365) (NEW) */
			prev_num = YEAR_LEN(work_year) - DAY_OF_YEAR(work_month,
					work_day, work_year);
			sprintf(buf, print_lz ? "%03d" : "%d", prev_num);
			break;

		case 'm':	/* %m : month (01-12) */
			prev_num = (work_month + adjust + 11) % 12 + 1;
			sprintf(buf, print_lz ? "%02d" : "%d", prev_num);
			break;

		case 'M':	/* %M : adjust date by <N> months (NEW) */
			if (!num_present || num_value == 0)
				RESET_DATE();
			else {
				int len;

				work_month += adjust * num_value;
				while (work_month > DEC) {
					work_month -= 12;
					work_year++;
				}
				while (work_month < JAN) {
					work_month += 12;
					work_year--;
				}

				/* make sure day of new month is legal */
				len = LENGTH_OF(work_month, work_year);
				if (work_day > len)
					work_day = len;
			}
			break;

		/* %u considers the week containing 1/1 to be week 1 and
		 * the next "logical Sunday" (the first day of the week
		 * as printed - cf. the -F option) to be the start of week
		 * 2; %U considers the first "logical Sunday" of the year to
		 * be the start of week 1.  %w and %W behave like %u and %U
		 * respectively, but use the first "logical Monday" instead.
		 */
		case 'W':	/* %W : week number (00-53)       */
			/* %W, if prefaced by [+-]N, adjusts the date by
			 * [+-]N weeks (resets if N == 0); check for this
			 * case first
			 */
			if (num_present) {
				if (num_value == 0)	/* N = 0: reset date */
					RESET_DATE();
				else
					SET_DATE(7 * adjust * num_value);
				break;
			}
			/* fall through */
		case 'u':	/* %u : week number (01-54) (NEW) */
		case 'U':	/* %U : week number (00-53)       */
		case 'w':	/* %w : week number (01-54) (NEW) */
			firstday = ((TOLOWER(c) == 'w' ? 15 : 14) -
					START_BOX(JAN, work_year)) % 7 + 1;
			prev_num = (DAY_OF_YEAR(work_month, work_day,
					work_year) - firstday + 7) / 7;
			if (islower(c) && firstday != 1)
				prev_num++;
			sprintf(buf, print_lz ? "%02d" : "%d", prev_num);
			break;

		case 'y':	/* %y : year w/o century (00-99) */
			prev_num = (work_year + adjust) % 100;
			sprintf(buf, "%02d", prev_num);
			break;

		case 'Y':	/* %Y : year w/century */
			/* %Y, if prefaced by [+-]N, adjusts the date by
			 * [+-]N years (resets if N == 0); check for this
			 * case first
			 */
			if (num_present) {
				if (num_value == 0)	/* N = 0: reset date */
					RESET_DATE();
				else {
					int len;

					work_year += adjust * num_value;

					/* make sure day is legal */
					len = LENGTH_OF(work_month, work_year);
					if (work_day > len)
						work_day = len;
				}
			} else {
				prev_num = work_year + adjust;
				sprintf(buf, "%d", prev_num);
			}
			break;

		/* prefix flags [o0+-] : set flags for next pass */

		case 'o':	/* %o : ordinal suffix (NEW) */
			ordinal = TRUE;
			break;

		case '0':	/* %0 : add leading zeroes (NEW) */
			print_lz = TRUE;
			break;

		case '+':	/* %+ : increment next value (NEW) */
		case '-':	/* %- : decrement next value (NEW) */
			adjust = c == '-' ? -1 : 1;
			if (isdigit(*p)) {		/* get the number */
				num_present = TRUE;
				while (isdigit(*p))
					num_value = num_value * 10 +
							(*p++ - '0');
			}
			break;

		case '\0':	/* accidental end-of-string */
		case ' ':
			return p - 1;

		default:	/* other - just copy it to output */
			sprintf(buf, "%c", c);
			break;
		};

	} while (strchr(prefixes, c) != NULL);

	/* append ordinal suffix if requested */
	if (ordinal && prev_num >= 0)
		strcat(buf, ordinal_suffix(prev_num));
	return p;

}


/*
 * print_word - print a single word, representing punctuation and non-ASCII
 * characters as octal literals and expanding format specifiers; return pointer
 * to character following word (NULL if no word follows)
 */
char *
#ifdef PROTOS
print_word(char *p)
#else
print_word(p)
	char *p;
#endif
{
	char c, buf[STRSIZ];
	int first = TRUE;	/* flag to avoid printing null strings */

	if (*p == '\0' || *(p += strspn(p, WHITESPACE)) == '\0')
		return NULL;

	while ((c = *p) && !isspace(c & CHAR_MSK)) {
		if (c == '%' && p[1] != '\0') {
			p = expand_fmt(buf, p + 1);
			if (*buf && first) {
				PRT("(");
				first = FALSE;
			}
			PUTSTR(isalnum, buf, stdout);	
		} else {
			if (first)
				PRT("(");
			first = FALSE;
			PUTCHAR(isalnum, c, stdout);
			p++;
		}
	}

	if (!first)
		PRT(")");

	return p;
}


/*
 * print_db_word - debug version of print_word; omits parentheses, does not
 * convert punctuation to escape sequences, and writes results to stderr
 * (not stdout)
 */
char *
#ifdef PROTOS
print_db_word(char *p)
#else
print_db_word(p)
	char *p;
#endif
{
	char c, buf[STRSIZ];

	if (*p == '\0' || *(p += strspn(p, WHITESPACE)) == '\0')
		return NULL;

	while ((c = *p) && !isspace(c & CHAR_MSK)) {
		if (c == '%' && p[1] != '\0') {
			p = expand_fmt(buf, p + 1);
			PUTSTR(isprint, buf, stderr);
		} else {
			PUTCHAR(isprint, c, stderr);
			p++;
		}
	}

	return p;
}


/*
 * print_pstext - print tokens in text (assumed separated by single blank)
 * in PostScript format and as debugging information if requested
 */
void
#ifdef PROTOS
print_pstext(char *p)
#else
print_pstext(p)
	char *p;
#endif
{
	char *s = p;		/* save for possible second pass */

	while (p = print_word(p))
		PRT(" ");

	/* repeat to generate debugging info if requested */
	if (debug_text)
		while (s = print_db_word(s))
			FPR(stderr, " ");

}


/*
 * print_text - print text as supplied; expand format specifiers but
 * do not tokenize into words or translate punctuation to escape sequences
 */
void
#ifdef PROTOS
print_text(char *p)
#else
print_text(p)
	char *p;
#endif
{
	char c, buf[STRSIZ];

	while (c = *p)
		if (c == '%' && p[1] != '\0') {
			p = expand_fmt(buf, p + 1);
			PUTSTR(isprint, buf, stdout);
		} else {
			PUTCHAR(isprint, c, stdout);
			p++;
		}
}


/*
 * def_footstring - print definition for foot string, again converting 
 * all characters other than letters, digits, or space to octal escape
 * and expanding format specifiers
 */
void
#ifdef PROTOS
def_footstring(char *p,			/* definition */
	       char *str)		/* name of string */
#else
def_footstring(p, str)
	char *p;			/* definition */
	char *str;			/* name of string */
#endif
{
	char c, buf[STRSIZ];

	this_day = 1;			/* set default day in foot string */
	RESET_DATE();			/* reset working date */

	PRT("/%s (", str);
	while (c = *p)
		if (c == '%' && p[1] != '\0') {
			p = expand_fmt(buf, p + 1);
			PUTSTR(isalnum, buf, stdout);
		} else {
			PUTCHAR(isalnum, c, stdout);
			p++;
		}
	PRT(") def\n");
}


/*
 * Routines to extract and print data
 */


/*
 * find_daytext - find and print the day (including notes) or holiday text
 * for the specified month/year
 */
void
#ifdef PROTOS
find_daytext(int month,
	     int year,
	     int is_holiday)	/* TRUE: print holiday text */
#else
find_daytext(month, year, is_holiday)
	int month, year;
	int is_holiday;		/* TRUE: print holiday text */
#endif
{
	register int day;
	year_info *py;
	month_info *pm;
	register day_info *pd;
	int first;
	char *fcn = is_holiday ? "holidaytext" : "daytext";
	char hol = is_holiday ? '*' : ' ';

	/* if no text for this year and month, return */

	if ((py = find_year(year, FALSE)) == NULL ||
	    (pm = py->month[month-1]) == NULL)
		return;

	/* walk array of day text pointers and linked lists of text */

	for (day = 1; day <= LAST_NOTE_DAY; day++) {
		for (pd = pm->day[day-1], first = TRUE;
		     pd;
		     pd = pd->next) {
			if (pd->is_holiday != is_holiday)
				continue;
			if (first) {
				PRT("%d [ \n", day >= FIRST_NOTE_DAY ?
					note_box(month, day, year) : day);
			}
			else {
				PRT("\n");
				print_word(LINE_SEP);	/* separate lines */
				PRT("\n");
			}
			this_day = day >= FIRST_NOTE_DAY ? 1 : day;
			RESET_DATE();		/* reset working date */
			if (debug_text)
				if (day < FIRST_NOTE_DAY)
					FPR(stderr, "%02d/%02d/%d%c ", month,
						day, year, hol);
				else
					FPR(stderr, "%02d[%02d]%d  ", month,
						day - FIRST_NOTE_DAY + 1, year);
			print_pstext(pd->text);
			if (debug_text)
				FPR(stderr, "\n");
			first = FALSE;
		}
		if (! first) {		/* wrap up call (if one made) */
			PRT("\n] %s\n", day >= FIRST_NOTE_DAY ? "notetext" :
								fcn);
		}
	}
}


/*
 * print_colors - print array specifying color of each date in current month
 * (formerly calculated on the fly in drawnums{} in pcalinit.ps)
 */
void
#ifdef PROTOS
print_colors(int month,
	      int year)
#else
print_colors(month, year)
	int month, year;
#endif
{
	register int day;
	year_info *py;
	month_info *pm;
	unsigned long holidays;
	int i, j, len;
	short color[32];

	len = LENGTH_OF(month, year);

	/* determine each date's color from its day of the week */
	for (day = 1, j = FIRST_OF(month, year);
	     day <= len;
	     day++, j = (j + 1) % 7) {
		color[day] = day_color[j];
	}

	pm = (py = find_year(year, FALSE)) ? py->month[month-1] : NULL;

	/* override weekday color for holidays */
	for (holidays = pm ? pm->holidays : 0, day = 1;
	     holidays;
	     holidays >>= 1, day++)
		if (holidays & 01)
			color[day] = holiday_color;

	PRT("/date_color [ -1");		/* dummy value for element 0 */
	for (day = 1; day <= len; day++)
		PRT("%s %d", day % 10 == 1 ? " " : "", color[day]);
	PRT(" ] def\n");
}


/*
 * find_noteboxes - find and print the note box numbers for specified
 * month/year
 */
void
#ifdef PROTOS
find_noteboxes(int month,
	       int year)
#else
find_noteboxes(month, year)
	int month, year;
#endif
{
	register int day;
	year_info *py;
	month_info *pm;

	/* if no text for this year and month, print empty list and return */

	if ((py = find_year(year, FALSE)) == NULL ||
	    (pm = py->month[month-1]) == NULL) {
		PRT("/noteboxes [ ] def\n");
		return;
	}

	PRT("/noteboxes [");	/* start definition of list */

	/* walk array of note text pointers, converting days to box numbers */

	for (day = FIRST_NOTE_DAY; day <= LAST_NOTE_DAY; day++)
		if (pm->day[day-1])
			PRT(" %d", note_box(month, day, year));
	PRT(" ] def\n");

}


/*
 * print_dates - print the dates (in "calendar" format) for the specified
 * month and year
 */
void
#ifdef PROTOS
print_dates(int month,
	    int year)
#else
print_dates(month, year)
	int month, year;
#endif
{
	register int day;
	year_info *py;
	month_info *pm;
	register day_info *pd;
	unsigned long holidays;
	int has_holiday_text;
	static int first = TRUE;
	static int save_year = 0;

	/* if no text for this year and month, return */

	if ((py = find_year(year, FALSE)) == NULL ||
	    (pm = py->month[month-1]) == NULL)
		return;

	/* print the year if it has changed */

	if (year != save_year)
		PRT("%s %d\n", kw_year, save_year = year);

	/* walk array of day text pointers and linked lists of text */

	for (holidays = pm->holidays, day = 1;
	     day < FIRST_NOTE_DAY;
	     holidays >>= 1, day++) {
		has_holiday_text = FALSE;
		for (pd = pm->day[day-1]; pd; pd = pd->next) {
			if (date_style == USA_DATES)
				PRT("%02d/%02d", month, day);
			else
				PRT("%02d/%02d", day, month);
			PRT(pd->is_holiday ? "* " : " ");
			this_day = day;
			RESET_DATE();	/* reset working date */
			print_text(pd->text);
			PRT("\n");
			has_holiday_text |= pd->is_holiday;
		}
		/* was date flagged as holiday w/o associated text? */
		if ((holidays & 01) && !has_holiday_text) {
			if (date_style == USA_DATES)
				PRT("%02d/%02d*\n", month, day);
			else
				PRT("%02d/%02d*\n", day, month);
		}
	}
}



/*
 * print_moon_info - print the information necessary to draw moons.  If
 * printing moons on all days, print the phase for each day; if printing
 * only quarter moons, tweak the phase to an exact quarter (so the icon
 * is printed correctly) and generate a list of the quarter-moon dates
 */
void
#ifdef PROTOS
print_moon_info(int month,
	        int year)
#else
print_moon_info(month, year)
	int month, year;
#endif
{
	int n, ndays, day, quarter;
	char *p;
	unsigned long qdays;
	double phase;
	static char *q[4] = {"NM", "1Q", "FM", "3Q"};

	if (draw_moons == NO_MOONS)
		return;

	/* print the phase of the moon for each day of the month */

	PRT("/moon_phases [\t\t%% from ");
	if ((p = find_moonfile(year)) != NULL)
		PRT ("%s", p);
	else {
		PRT("algorithm");
		if (atof(time_zone) != 0.0)
			PRT(" (UTC offset = %s)", time_zone);
	}
	PRT("\n\t");

	for (n = 0, qdays = 0L, day = 1, ndays = LENGTH_OF(month, year);
	     day <= ndays;
	     day++) {
		phase = find_phase(month, day, year, &quarter);
		if (DEBUG(DEBUG_MOON))
			FPR(stderr, "%02d/%02d/%d: %.3f %s\n", month, day,
				year, phase, quarter != MOON_OTHER ?
				q[quarter] : "");
		/* adjust phase to exact quarter if printing only quarters */
		if (draw_moons == SOME_MOONS && quarter != MOON_OTHER)
			phase = 0.25 * quarter;
		if (draw_moons == ALL_MOONS || quarter != MOON_OTHER)
			PRT("%.3f%s", PRT_TWEAK(phase), ++n % 10 == 0 ?
			    "\n\t" : " ");
		if (quarter != MOON_OTHER)
			qdays |= 1L << (day - 1);
	}
	PRT("] def\n");

	/* if drawing only quarter moons, print days when they occur */

	if (draw_moons == SOME_MOONS) {
		PRT("/quarter_moons [ ");
		for (day = 1; qdays; day++, qdays >>= 1)
			if (qdays & 01)
				PRT("%d ", day);
		PRT("] def\n");
	}
}


/*
 * print_julian_info - print the information necessary to print Julian dates
 */
void
#ifdef PROTOS
print_julian_info(int month,
		  int year)
#else
print_julian_info(month, year)
	int month, year;
#endif
{

	if (julian_dates != NO_JULIANS)
		PRT("/jdstart %d def\n", DAY_OF_YEAR(month, 1, year));
	if (julian_dates == ALL_JULIANS)
		PRT("/yearlen %d def\n", YEAR_LEN(year));
}


/*
 * print_month - generate calendar for specified month/year
 */
void
#ifdef PROTOS
print_month(int month,
	    int year)
#else
print_month(month, year)
	int month, year;
#endif
{
	static int nmonths = 0, page = 0;
	int startbox;

	/* start of new physical page? */
	if (!do_whole_year || nmonths % 12 == 0)
		PS_STARTPAGE();

	PRT("/year %d def\n", year);		/* set up year and month */
	PRT("/month %d def\n", month);

	/* move starting box to second row if conflict with small calendars */
	startbox = START_BOX(month, year);
	if (!do_whole_year &&
	    (prev_cal_box[small_cal_pos] == startbox ||
	     next_cal_box[small_cal_pos] == startbox) )
		startbox += 7;
	PRT("/startbox %d def\n", startbox);
	PRT("/ndays %d def\n", LENGTH_OF(month, year));

	find_noteboxes(month, year);		/* make list of note boxes */
	print_colors(month, year);		/* make list of date colors */

	/* are we printing 12 months per page or only one? */

	if (do_whole_year) {
		/* reset foot strings at start of each page */
		if (nmonths % 12 == 0) {
			def_footstring(lfoot, "Lfootstring");
			def_footstring(cfoot, "Cfootstring");
			def_footstring(rfoot, "Rfootstring");
			def_footstring(notes_hdr, "notesheading");
		}

		PRT("/posn %d def\n", nmonths % 12);	/* location on page */
		PRT("printmonth\n");
	}
	else {
		/* reset foot strings each month (may change) */
		def_footstring(lfoot, "Lfootstring");
		def_footstring(cfoot, "Cfootstring");
		def_footstring(rfoot, "Rfootstring");
		def_footstring(notes_hdr, "notesheading");

		/* generate information necessary for small calendars */

		if (small_cal_pos != SC_NONE) {
			int m, y;

			PRT("/p_year %d def\n", y = PREV_YEAR(month, year));
			PRT("/p_month %d def\n", m = PREV_MONTH(month, year));
			PRT("/p_startbox %d def\n", START_BOX(m, y));
			PRT("/p_ndays %d def\n", LENGTH_OF(m, y));

			PRT("/n_year %d def\n", y = NEXT_YEAR(month, year));
			PRT("/n_month %d def\n", m = NEXT_MONTH(month, year));
			PRT("/n_startbox %d def\n", START_BOX(m, y));
			PRT("/n_ndays %d def\n", LENGTH_OF(m, y));
		}

		print_julian_info(month, year);		/* Julian date info */
		print_moon_info(month, year);		/* moon info */

		PRT("printmonth\n");
		find_daytext(month, year, TRUE);	/* holiday text */
		find_daytext(month, year, FALSE);	/* day and note text */
	}

	/* end of physical page? */
	if (!do_whole_year || ++nmonths % 12 == 0)
		PS_ENDPAGE();
}
