/*
 *	calendar.c:
 *	generates a giant calendar for a full Gregorian cycle of 400 years.
 *	Each date is given individually as YYYY-MM-DD, making it possible
 *	for systems like Crit to locate and annotate any particular date!
 *	Writes to the standard output.
 *
 *	usage: calendar <year-to-start-on>  (year should be in range 0200-9800)
 *
 *	We start at the given year, continue forwards in time year by year,
 *	jump abruptly back one cycle length at the halfway point,
 *	and thus end up at the year before the given year.
 *	Thus the {present / near future} and recent past of the given year
 *	are at easily accessible "distinguished places" in the output
 *	(the beginning and end respectively), while the abrupt jump is
 *	hidden away in the probably rather under-used amorphous middle zone.
 *
 *
 *	NOTE: This source code is not very polished.
 */

#include <stdio.h>


#define bool	int
#define TRUE	1
#define FALSE	0


#define YEARS_IN_GREGORIAN_CYCLE	400
#define Y10K_OVERFLOW_YEAR		10000
#define MIN_VALID_YEAR			(YEARS_IN_GREGORIAN_CYCLE / 2)
#define MAX_VALID_YEAR			(Y10K_OVERFLOW_YEAR - \
						YEARS_IN_GREGORIAN_CYCLE / 2)

#define MONTHS_IN_YEAR			12
#define DAYS_IN_WEEK			7
#define SATURDAY_TO_MONDAY_OFFSET	2

#define WHITESPACE_BEFORE_MONTH_NAME	"                               "
#define WHITESPACE_MATCHING_YYYY_MM_DD	"          "


/* private forward references */
static int  day_of_week_from_Saturday (int year, int month, int date);
static int  days_in_month (int month, int year);
static bool is_leap_year (int year);




void main(int argc, char *argv[])
{
	static char *MONTH_NAME[MONTHS_IN_YEAR] = { " January",
						    " February",
						    "  March",
						    "  April",
						    "   May",
						    "   June",
						    "   July",
						    "  August",
						    "September",
						    " October",
						    " November",
						    " December" };

	static char *DAY_OF_WEEK_FROM_MONDAY_NAME[DAYS_IN_WEEK]
						= { "  Monday  ",
						    " Tuesday  ",
						    "Wednesday ",
						    " Thursday ",
						    "  Friday  ",
						    " Saturday ",
						    "  Sunday  " };

	char *base_year_argument;
	int  base_year, unadjusted_offset, offset, year, month, date,
		day_of_week_from_Monday_of_1st_of_month,
		day_of_week_from_Monday;
	char continuation_char;

	/*
	 *	argument checking
	 */
	if (argc != 2) {
		fprintf(stderr, "usage: %s <year-to-start-on>", argv[0]);
		fprintf(stderr, "  (year should be in range %.4d-%.4d)\n",
				MIN_VALID_YEAR, MAX_VALID_YEAR);
		exit(1);
	}/*endif*/

	base_year_argument = argv[1];

	/*
	 *	do we have a syntactically valid numeric year as argument?
	 */
	if (sscanf(base_year_argument, "%d", &base_year) != 1) {
		fprintf(stderr,
			"%s: \"%s\" not a valid numeric year!\n",
			argv[0],
			base_year_argument);
		fprintf(stderr, "usage: %s <year-to-start-on>", argv[0]);
		fprintf(stderr, "  (year should be in range %.4d-%.4d)\n",
				MIN_VALID_YEAR, MAX_VALID_YEAR);
		exit(1);
	}/*endif*/

	/*
	 *	is the year in the range we can cope with?
	 */
	if (base_year < MIN_VALID_YEAR || base_year > MAX_VALID_YEAR) {
		fprintf(stderr,
			"%s: year %d not in range %.4d-%.4d!\n",
			argv[0],
			base_year, MIN_VALID_YEAR, MAX_VALID_YEAR);
		fprintf(stderr, "usage: %s <year-to-start-on>", argv[0]);
		fprintf(stderr, "  (year should be in range %.4d-%.4d)\n",
				MIN_VALID_YEAR, MAX_VALID_YEAR);
		exit(1);
	}/*endif*/

	/*
	 *	OK, let's generate the giant calendar!
	 */
	for (unadjusted_offset = 0;
	     unadjusted_offset < YEARS_IN_GREGORIAN_CYCLE;
	     unadjusted_offset++) {
		offset = unadjusted_offset < YEARS_IN_GREGORIAN_CYCLE / 2
				? unadjusted_offset
				: unadjusted_offset - YEARS_IN_GREGORIAN_CYCLE;
		year = base_year + offset;

		/*
		 *	we have a current year:
		 *	now go through its months in turn.
		 */
		for (month = 1; month <= MONTHS_IN_YEAR; month++) {
			/*
			 *	print a header for the current month
			 */
			printf("%s%s %.4d\n", WHITESPACE_BEFORE_MONTH_NAME,
						MONTH_NAME[month-1], year);

			/*
			 *	print the days of the week along the top
			 */
			for (day_of_week_from_Monday = 0;
			     day_of_week_from_Monday < DAYS_IN_WEEK;
			     day_of_week_from_Monday++) {
				continuation_char =
					day_of_week_from_Monday ==
							DAYS_IN_WEEK - 1
					? '\n' : ' ';

				printf("%s%c",
					DAY_OF_WEEK_FROM_MONDAY_NAME
						[day_of_week_from_Monday],
					continuation_char);
			}/*end for day_of_week_from_Monday*/

			/*
			 *	get to the right place for the 1st of the month
			 */
			day_of_week_from_Monday_of_1st_of_month =
				(day_of_week_from_Saturday(year, month, 1)
					- SATURDAY_TO_MONDAY_OFFSET
					+ DAYS_IN_WEEK
				) % DAYS_IN_WEEK;

			for (day_of_week_from_Monday = 0;
			     day_of_week_from_Monday <
				day_of_week_from_Monday_of_1st_of_month;
			     day_of_week_from_Monday++) {
				printf("%s ", WHITESPACE_MATCHING_YYYY_MM_DD);
			}/*end for day_of_week_from_Monday*/

			/*
			 *	now go through its dates, throwing a newline
			 *	at the end of each Monday-to-Sunday week
			 *	and on the final day of the month.
			 */
			for (date = 1;
			     date <= days_in_month(month, year);
			     date++) {
				day_of_week_from_Monday =
					(day_of_week_from_Monday_of_1st_of_month
						+ (date - 1)
					) % DAYS_IN_WEEK;

				continuation_char = (date ==
						     days_in_month(month, year)
							||
						     day_of_week_from_Monday ==
						     DAYS_IN_WEEK - 1
						    ) ? '\n' : ' ';

				printf("%.4d-%.2d-%.2d%c",
					year, month, date, continuation_char);
			}/*end for date*/

			/*
			 *	final newline to separate months more clearly.
			 */
			printf("\n");
		}/*end for month*/
	}/*end for year (in effect anyway)*/

	exit(0);
}




static int day_of_week_from_Saturday (int year, int month, int date)
/* my venerable old "canonical" day-of-week algorithm. */
{
	static int CENTURY_PART_TABLE[4]              = {6,4,2,0};
	static int   MONTH_PART_TABLE[MONTHS_IN_YEAR] = {1,4,4,0,2,5,0,3,6,1,4,6};

	int     century_part = CENTURY_PART_TABLE[year / 100 % 4];
	int Julian_leap_part = year % 100 / 4 * (-2 + DAYS_IN_WEEK);
	int        year_part = year % 4;
	int       month_part = MONTH_PART_TABLE[month-1];
	int        premature = month <= 2 && is_leap_year(year) ? -1 : 0;

	return (century_part + Julian_leap_part + year_part +
		month_part + date + premature
	       ) % DAYS_IN_WEEK;
}




static int days_in_month (int month, int year)
/* reports the number of days in the given month of the given year. */
{
	switch (month) {
		case  1 : return                           31; break;
		case  2 : return is_leap_year(year) ? 29 : 28; break;
		case  3 : return                           31; break;
		case  4 : return                           30; break;
		case  5 : return                           31; break;
		case  6 : return                           30; break;
		case  7 : return                           31; break;
		case  8 : return                           31; break;
		case  9 : return                           30; break;
		case 10 : return                           31; break;
		case 11 : return                           30; break;
		case 12 : return                           31; break;
		default : fprintf(stderr,
				  "days_in_month: illegal month value %d!\n",
				  month);
			  exit(1);
			  break;
	}/*endswitch*/
}




static bool is_leap_year (int year)
/* reports whether the given year is a Gregorian leap year. */
{
	return (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0);
}
