#!/usr/bin/perl

# classgrid, v1.2, by Dan Wallach <dwallach@soda.berkeley.edu>
#
# using PostScript code from Brent Chapman <brent@telebit.com>

# v1.2, 29 Jan    1992 -- added -brief, other small changes
# v1.1, 16 August 1991 -- added Postscript support
# v1.0, 15 August 1991 -- initial version

# this program takes your schedule of classes, in basically the same
# format as the ACE form, and generates one of those pretty grids which
# are so useful...
#
# You simply make a file in the below format and run
# classgrid [-ps] [-brief] filename [filename...]
# (filename can be '-' if you want standard input)
#
# The -ps flag causes PostScript code to be generated, rather than
# ASCII text graphics.
#
# The -brief flag causes brief output for text output (-ps -brief = -ps).
# this is useful for inclusion in .plan files.
#
# There are four valid line styles:
#
# @command [argument]
# >class|title|teacher
# class|type|days|hours|hall
# # <any comment>
#
# You start only one line per class with the > sign, and this gives the
# full title of the class and the teacher's name if you know it.
#
# You give one normal line for each class section.  Currently, valid types
# are Lec, Dis, and Lab.  If you want something else, go stick it in
# %class_types and @class_types_short in the code.
#
# Valid days are: M T W Th and F (you must have whitespace between them)
#
# Valid hours are from 8 am to 8 pm and are written without any am or pm
#   markings.  Just say 11-2 and the right things happens.  If you happen to
#   need exactly 8pm, you'll have to write '8p' otherwise, you're fine.
#   YES, DEAR, it's a hack. :-)
#
# Valid @ commands are currently
# @gray [graylevel]
# @title [string]
#
# the graylevel goes between zero and one, with one being pure white, and
# zero being pure black -- this is the background of the shaded boxes
# and applies to all boxes starting where you set the graylevel.  By
# default, it's something like .95
#
# the title goes atop the whole schedule and you only get one of them.

#
# Perl-related info: this program doesn't adhere to any particular style
# or method, but rather is just sort of a mishmash of different Perl tricks
# to get the job done.  It's ugly, it's a hack, but it works pretty well.
# I use at least three different ways of doing multi-dimensional arrays.
#

$[ = 1;

@possible_hours = (
	   "8",  "8:30",
	   "9",  "9:30",
	  "10", "10:30",
	  "11", "11:30",
	  "12", "12:30",
	   "1",  "1:30",
	   "2",  "2:30",
	   "3",  "3:30",
	   "4",  "4:30",
	   "5",  "5:30",
	   "6",  "6:30",
	   "7",  "7:30",
	   "8p" );

@ps_hour_map = (         # converting to military time
	   "8",  "8.5",
	   "9",  "9.5",
	  "10", "10.5",
	  "11", "11.5",
	  "12", "12.5",
	  "13", "13.5",
	  "14", "14.5",
	  "15", "15.5",
	  "16", "16.5",
	  "17", "17.5",
	  "18", "18.5",
	  "19", "19.5",
	  "20", "20.5",
	  "21");

$earliest = $#possible_hours + 1;
$latest = $[ - 1;

@possible_days = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday" );
@possible_days_short = ("M", "T", "W", "Th", "F");

@class_types_short = ( "Lec", "Dis", "Lab" , "Sem");

%class_types = (
	"Lec", "Lecture",
	"Sem", "Seminar",
	"Dis", "Discussion",
	"Lab", "Laboratory");

$entry_width = 11;  # plus margins
$hour_width = 5;    # plus margins
$table_width = ($entry_width + 3)*(@possible_days) + $hour_width + 2;

#
# why isn't this in Perl???  Seems Larry isn't into float->int conversions
#
sub ceil {
    local($float) = @_;

    if (int($float) == $float || $float < 0) {
	return (int($float));
    }

    return(int($float) + 1);
}

sub store_grid {
    local($day_index, $start_index, $end_index, $class, $type, $hall) = @_;

    $earliest = $start_index if $start_index < $earliest;
    $latest = $end_index - 1 if $end_index - 1 > $latest;

    eval <<EOSTUFF;
	\$$possible_days[$day_index][$start_index] =
	    "$class|$class_types{$type}|$hall";
	for(($start_index+1) .. ($end_index-1)) {
	    \$$possible_days[$day_index][\$_] =
		"v" x $entry_width . "|" .
		"v" x $entry_width . "|" .
		"v" x $entry_width;
	}
EOSTUFF
}

#
# create hash table indices into the possible hours array
#
for($[ .. $#possible_hours) {
    $possible_hours{$possible_hours[$_]} = $_;
}

#
# and again for indices into the possible_days arrays
#
for($[ .. $#possible_days) {
    $possible_days{$possible_days[$_]} = $_;
    $possible_days_short{$possible_days_short[$_]} = $_;
}

# default is full text
$full = 1;

#
# okay, finally, the main loop
#

foreach(@ARGV) {
    if (/-ps/) {
	$generate_postscript = 1;
	next;
    }

    if (/-brief/) {
	$full = 0;
	next;
    }

    if(/^-$/) {
	open(MYFILE, "<&STDIN") || die "Can't dup STDIN: $!";
    }
    else {
	open(MYFILE, "$_") || die "Can't open $_: $!";
    }

    mainloop: while(<MYFILE>) {

	chop;
	s/#.*$//;
	next if /^$/;

	if(/^>/) {
	    s/^>//;
	    ($class, $title, $teacher) = split(/\|/);
	    push(@classes, ($class));
	    $dbase{$class, "title"} = $title;
	    $dbase{$class, "teacher"} = $teacher;
	    next mainloop;
	}

	if(/^@/) {
	    s/@(\S+) //;	# throw away the command
	    if($1 eq 'title') {
		push(@ps_output,  "($_) Title\n");
		$PageTitle = $_;
		next mainloop;
	    }

	    if($1 eq 'gray' || $1 eq 'grey') {
		push (@ps_output,  "$_  setgray\n");
		next mainloop;
	    }

	    print STDERR "Warning: unknown @ command: $1\n";
	    next mainloop;
	}

	if(/^#/) { 
           # discard comments!
	   next mainloop;
        }

	($class, $type, $days, $hours, $hall, $teacher) = split(/\|/);
	if (!($class_types{$type})) {$class_types{$type}=$type;} # Hack to do new types
	$dbase{$class,$type} = sprintf("%10s: %7s  %-12s %s",
				   $class_types{$type}?$class_types{$type}:$type, 
				   $days, $hours, $hall);

	#
	# now, find the hourly indices
	#

	($start, $end) = split(/\s*-\s*/, $hours);

	$start_index = $possible_hours{$start} || die "Malformed class hours: $start";
	$end_index = $possible_hours{$end} || die "Malformed class hours: $end";

	die "Bad class hours: starts after it ends ($start - $end)"
	    if $start_index >= $end_index;

	#
	# now, find which days we have the class on
	#

	@day_array = split(/\s+/, $days);

	foreach (@day_array) {
	    $day_index = $possible_days_short{$_} || die "Malformed class day: \`$_\'";
	    &store_grid($day_index, $start_index, $end_index,
			$class, $type, $hall);

	    push(@ps_output, "[ ($class) ($class_types{$type}) ($hall) ] $ps_hour_map[$start_index] $ps_hour_map[$end_index] $possible_days[$day_index] Box\n");
	}
    }
    close(MYFILE);
}

if($generate_postscript) {
    #
    # generate PS output...
    #

###    $BoxesPerPage = &ceil(($latest - $earliest)/2.0);
    $EarliestHour = int($ps_hour_map[$earliest]);
    $LatestHour = &ceil($ps_hour_map[$latest+1]);
    $BoxesPerPage = $LatestHour - $EarliestHour;

    print <<EOSTUFF;
%!

% This PostScript generated by psgrid, v1.1
%    by Dan Wallach <dwallach@soda.berkeley.edu>
%
% This PS Prologue was written by Brent Chapman <brent@telebit.com>

/BoxesPerPage $BoxesPerPage def
/EarliestHour $EarliestHour def
/LatestHour $LatestHour def

EOSTUFF

    #
    # snarf the whole prologue
    #
    while(<DATA>) { print; }   

    #
    # send out all the filled boxes
    #
    print @ps_output;		

    #
    # put the class titles at the bottom
    #

    print <<EOSTUFF;

%
% now, the class titles...
%
0 setgray		% full black text
/Helvetica findfont 14 scalefont setfont  % 14 point Helvetica

EOSTUFF
    $linenumber = 1;
    $spacing = 1.5 / @classes;
    foreach (@classes) {
	print "BaseX BaseY $spacing inch $linenumber mul sub moveto\n";
    	print "($_: $dbase{$_,'title'}";
	print ", $dbase{$_,'teacher'}"     if $dbase{$_,'teacher'};
    	print ") show\n";
	$linenumber++;
    }

    print "\nshowpage\n";
}

else {
    if ($full) {
	print "$PageTitle\n\n" if defined($PageTitle);
	
	foreach (@classes) {
	    print "$_: $dbase{$_,'title'}";
	    print ", $dbase{$_,'teacher'}" if $dbase{$_,'teacher'};
	    print "\n";
	    
	    foreach $ctype (@class_types_short) {
		print $dbase{$_, $ctype}, "\n" if $dbase{$_, $ctype};
	    }
	    
	    print "\n";
	}
    }

    print "-" x $table_width, "\n";
    print " " x $hour_width, " ";
    foreach(@possible_days) {
        printf "| %${entry_width}s ", $_;
    }
    print "\n";
    print "-" x $table_width, "\n";
    
    foreach $hour_index ($earliest .. $latest) {
        $hour = $possible_hours[$hour_index];
    
        print "-" x $table_width, "\n" if ($full);
        printf "%-${hour_width}s ", $hour;
    
        foreach $day (@possible_days) {
	    $day_index = $possible_days{$day};
	    eval <<EOSTUFF;
    	    \$entries = \$$possible_days[$day_index][$hour_index];
EOSTUFF
		
		@entries = split(/\|/, $entries);
	    for($[..$[+2) {
		$entry[$day_index * 3 + $_] = $entries[$_];
	    }
        }

	if ($full) {
	    for ($[..$[+2) {
		print  " " x ($hour_width + 1) if $_ != $[;
		foreach $day (@possible_days) {
		    $day_index = $possible_days{$day};
		    printf "| %${entry_width}.${entry_width}s ",
		    $entry[$day_index * 3 + $_];
		}
		print "\n";
	    }
	}
	else {
		foreach $day (@possible_days) {
		    $day_index = $possible_days{$day};
		    printf "| %${entry_width}.${entry_width}s ",
		    $entry[$day_index * 3 + 1];
		}
		print "|\n";
	}

    }
    print "-" x $table_width, "\n";
}
    


__END__

%  BoxesPerPage -- how many total hours, from start to finish we're in class
%  EarliestHour -- 24hr. time of earliest class
%  LatestHour -- 24hr. time of latest class

/inch { 72 mul } def

/BaseX .75 inch def
/BaseY 1.9 inch def

/TextSize 12 def                    % 12 point text for most stuff

/BoxX 1.45 inch def
/BoxY 8 BoxesPerPage div inch def

.1 setlinewidth

0 1 5 {
			% n
    BoxX mul 		% x_offset
    BaseX add		% x
    BaseY moveto	% -
    0 BoxY BoxesPerPage mul 	% 0 y_offset
    rlineto		% -
    stroke		% -

} for

0 1 BoxesPerPage {
			% n
    BoxY mul		% y_offset
    BaseY add		% y
    BaseX exch moveto	% -
    BoxX 5 mul 0	% x_offset 0
    rlineto		% -
    stroke		% -

} for

/TextFont /Helvetica findfont TextSize scalefont def
/TitleFont /Helvetica-Bold findfont 36 scalefont def

TextFont setfont

/daynum 0 def

[
    (Monday)
    (Tuesday)
    (Wednesday)
    (Thursday)
    (Friday)
] {

    daynum BoxX mul BaseX add .1 inch add	% (str) x
    BaseY BoxY BoxesPerPage mul add .1 inch add		% (str) x y
    moveto					% (str)
    show					% -
    daynum 1 add /daynum exch def

} forall

/str 3 string def

EarliestHour 1 LatestHour {
    dup				% n n
    EarliestHour sub		% n n'
    BoxesPerPage exch sub	% n n"
    BoxY mul BaseY add		% n y
    4 sub			% n y'
    BaseX .25 inch sub		% n y' x
    exch moveto			% n
    dup 12 gt			% n bool
    {			
				% n
	12 sub			% n'
    } if
    str cvs show		% -
} for

/Box {	% [ (message) ... ] start_time stop_time day
    
    BoxX mul BaseX add 0 moveto		% [ (msg) ... ] start_time stop_time
    EarliestHour sub BoxesPerPage exch sub			% [ (msg) ... ] start_time stop_ofs
    BoxY mul BaseY add 0 exch rmoveto	% [ (msg) ... ] start_time
    currentpoint newpath moveto		% [ (msg) ... ] start_time
    BoxX 0 rlineto			% [ (msg) ... ] start_time
    EarliestHour sub BoxesPerPage exch sub			% [ (msg) ... ] start_ofs
    BoxY mul BaseY add 			% [ (msg) ... ] y
    currentpoint exch pop		% [ (msg) ... ] y y'
    sub 0 exch rlineto			% [ (msg) ... ]
    BoxX neg 0 rlineto			% [ (msg) ... ] x y
    currentpoint			% [ (msg) ... ] x y
    closepath 				% [ (msg) ... ] x y
    gsave fill grestore 		% [ (msg) ... ] x y
    /gray currentgray def		% [ (msg) ... ] x y
    0 setgray stroke 			% [ (msg) ... ] x y
    moveto				% [ (msg) ... ]
    .05 inch -.05 inch TextSize sub rmoveto	% [ (msg) ... ]
    {
	currentpoint			% (msg) x y
	/Y exch def /X exch def		% (msg)
	show				% -
	X Y moveto 0 TextSize neg rmoveto	% -
    } forall
    gray setgray
	
} def

/Title {			% (str)
    TitleFont setfont		% (str)
    /gray currentgray def	% (str)
    0 setgray 			% (str)
    BaseX BoxesPerPage BoxY mul BaseY add	% (str) x y
    .4 inch add moveto		% (str)
    show			% -
    gray setgray
    TextFont setfont
} def


/am { } def
/pm { 12 add } def 

/Monday 	0 def
/Tuesday	1 def
/Wednesday	2 def
/Thursday	3 def
/Friday		4 def

.95 setgray

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%									      %
% 	And, that's it for the preamble, now the user data.                   %
%									      %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


