#!/bin/ksh
# bigcal: print a big calendar
# @(#) bigcal.ksh 2.1 96/11/13
# 90/06 John H. DuBois III (john@armory.com)
# 91/01/13 cleaned up slightly.
# 95/03/23 Print help without pausing.  Added description of f option to help.
#          Changed argument format.
# 95/08/01 Added s option.
# 96/11/13 Added X option.

# Prints a calendar with squares, with digits printed in various fonts,
# and optionally filled with data.
# Uses 'cal' to get the digits.
# Use -h to for help.

# The output of "cal" is expected to be in a format like this:

#   June 1990
# S  M Tu  W Th  F  S
#                1  2
# 3  4  5  6  7  8  9
#10 11 12 13 14 15 16
#17 18 19 20 21 22 23
#24 25 26 27 28 29 30

# Set defaults.  The meaning of these vars is described in help.
box=ascii
t=- b=- l=\| r=\| tl=- bl=- tr=- br=- m=+ h=- v=\|	# Set ascii box chars.
hsize=9
vsize=5
digits=1
ReadFields=false
short=false
debug=false

lname=${0##*/}
Usage=\
"Usage: $lname [-hfs] [-b<boxtype>] [-d<digits>] [-c<name=value[,...]>]
              [-x<width>] [-y<height>] [<month>]"

while getopts :fhb:d:x:y:c:sX opt; do
    case $opt in
    h)
	echo \
"$lname: print a big (box-style) calendar.
$Usage
$lname prints a calendar for month <month>.  <month> should be in the same
format as that recognized by /bin/cal, except that a single numeric argument
for month will be interpreted as a month number rather than a year, and a
year given as two digits is assumed to be in the century 19xx.  If <month>
is not given, a calendar for the current month is printed.
Options:
-h: print this help.
-s: Merge the date header into the first box line.
-f: Read data to be printed in boxes from the standard input.  The first line
    read is printed in the box for the first day of the month, etc.  Each
    word on the line is printed on a separate line in the box.
-b<boxtype>: <boxtype> should be either ascii, ibm1, or ibm2.  The calendar
    boxes are printed using characters from the specified set.  ascii is the
    default and will work on any ascii device.  ibm1 and ibm2 look much better
    but will only work on devices that implement the IBM extended character
    set; for example, a PC console, IBM-compatible graphic printers, and Wyse
    60 terminals.
-d<digits>: <digits> should be one of 1, 5, ibm1_3, ibm1_5, ibm2_3, or ibm2_5.
    Date digits are drawn using the specified font.  1 is the normal
    single-character digits; 5 is a font drawn with ascii characters that is 5
    characters high; ibmn_h is a font drawn using the n-line box drawing
    characters that is h characters high.  The ibmn_h fonts work on devices as
    described for box.
-c<name=value[,...]>: Set the box drawing characters.  This option should not
    be used with -b.  The assignments are done by a comma-separated string of
    name=value pairs.  The names and the box components they set are:
    t, b, l, r: used where a bar joins with the outer top, left, bottom, and
    right bar of the calendar.
    tl, bl, tr, br: used for the top left, bottom left, top right, and bottom
    right corner of the calendar.
    m: middle; used where a horizontal and vertical bar cross each other.
    h, v: used for drawing horizontal and vertical bars.
-x<width>: The interior width of individual date boxes is set to <width>.  The
    default is $hsize.
-y<height>: The interior height of individual date boxes is set to <height>. 
    The default is $vsize."
	exit 0
	;;
    f)
	ReadFields=true;;
    b)
	box=$OPTARG;;
    d)
	digits=$OPTARG;;
    x)
	hsize=$OPTARG;;
    X)
	debug=true;;
    y)
	vsize=$OPTARG;;
    c)
	boxchars=$OPTARG;;
    s)
	short=true;;
    +?)
	print -u2 "$lname: options should not be preceded by a '+'."
	exit 1
	;;
    :) 
	print -u2 "$lname: Option '$OPTARG' requires a value.  Use -h for help."
	exit 1
	;;
    ?) 
	print -u2 "$lname: $OPTARG: bad option.  Use -h for help."
	exit 1
	;;
    esac
done
 
# remove args that were options
let OPTIND=OPTIND-1
shift $OPTIND

case $# in
0)
    month=`date +%h`	# default month is current month
    ;;
1)
    month=$1
    # if month is given numerically without a year, add year to it because
    # /bin/date assumes a single numeric argument is a year.
    [[ "$month" = +([0-9]) ]] && month="$month "`date +19%y`
    ;;
2)
    [[ "$2" = [0-9][0-9] ]] && month="$1 19$2" || month="$*"
    ;;
*)
    print -u2 "$Usage\nUse -h for help."
    exit 1
    ;;
esac

if [ -n "$boxchars" ]; then
    OIFS=$IFS
    IFS=,
    set -- $boxchars
    IFS=$OIFS
    for arg; do
	case $arg in
	    @([tblrmhv]|[tb][lr])=*) eval ${arg%%=*}=\"${arg#*=}\";;
	esac
    done
fi

# set chars for drawing box-style digits.  Names explained in help data.
case $digits in
ibm2_*)
    t="\0313" b="\0312" l="\0314" r="\0271" tl="\0311" bl="\0310"
    tr="\0273" br="\0274" m="\0316" h="\0315" v="\0272"
    ;;
ibm1_*)
    t="\0302" b="\0301" l="\0303" r="\0264" tl="\0332" bl="\0300"
    tr="\0277" br="\0331" m="\0305" h="\0304" v="\0263"
    ;;
esac

# Set arrays n0..n9 and nb (blank) to digit fonts.
# Each element of the array is a row of chars for the digit.
# digheight is the number of rows in each digit. 
# gap is the string used to separate two digits.
# digwidth is the width of two digits + gap
case $digits in
1)
    digheight=1 digwidth=2 gap=
    n0=0 n1=1 n2=2 n3=3 n4=4 n5=5 n6=6 n7=7 n8=8 n9=9 nb=' '
    ;;
5)
    digheight=5 digwidth=9 gap=' '
    set -A n0 \
' __ ' \
'/  \' \
'|  |' \
'|  |' \
'\__/'

    set -A n1 \
'    ' \
'  | ' \
'  | ' \
'  | ' \
'  | '

    set -A n2 \
' __ ' \
'/  |' \
'  / ' \
' /  ' \
'/___'

    set -A n3 \
' __ ' \
'/  \' \
'  _/' \
'   \' \
'\__/'
    set -A n4 \
'    ' \
'| | ' \
'|_|_' \
'  | ' \
'  | '

    set -A n5 \
'____' \
'|   ' \
'|__ ' \
'   \' \
'\__/'

    set -A n6 \
' __ ' \
'/  `' \
'|__ ' \
'|  \' \
'\__/'

    set -A n7 \
'____' \
'   /' \
'  / ' \
' /  ' \
'/   '

    set -A n8 \
' __ ' \
'/  \' \
'\__/' \
'/  \' \
'\__/'

    set -A n9 \
' __ ' \
'/  \' \
'\__|' \
'   |' \
'\__/'

    set -A nb \
'    ' \
'    ' \
'    ' \
'    ' \
'    '
    ;;
ibm1_3|ibm2_3)
    digheight=3 digwidth=5 gap=' '
    set -A n0 -- \
"$tl$tr" \
"$v$v" \
"$bl$br"

    set -A n1 -- \
"$tr " \
"$v " \
"$b "

    set -A n2 -- \
" $tr" \
"$tl$br" \
"$bl "

    set -A n3 -- \
"$tr " \
"$r " \
"$br "

    set -A n4 -- \
"$v$v" \
"$bl$r" \
" $v"

    set -A n5 -- \
"$tl " \
"$bl$tr" \
" $br"

    set -A n6 -- \
"$tl " \
"$l$tr" \
"$bl$br"

    set -A n7 -- \
"$h$tr" \
" $v" \
" $v"

    set -A n8 -- \
"$tl$tr" \
"$l$r" \
"$bl$br"

    set -A n9 -- \
"$tl$tr" \
"$bl$r" \
" $v"

    set -A nb -- \
"  " \
"  " \
"  "
	;;
ibm1_5|ibm2_5)
    digheight=5 digwidth=7 gap=' '
    set -A n0 -- \
"$tl$h$tr" \
"$v $v" \
"$v $v" \
"$v $v" \
"$bl$h$br"

    set -A n1 -- \
" $tr " \
" $v " \
" $v " \
" $v " \
" $b "

    set -A n2 -- \
" $h$tr" \
"  $v" \
"$tl$h$br" \
"$v  " \
"$bl$h "

    set -A n3 -- \
" $h$tr" \
"  $v" \
" $h$r" \
"  $v" \
" $h$br"

    set -A n4 -- \
"$v $v" \
"$v $v" \
"$bl$h$r" \
"  $v" \
"  $v"

    set -A n5 -- \
"$tl$h " \
"$v  " \
"$bl$h$tr" \
"  $v" \
" $h$br"

    set -A n6 -- \
"$tl$h " \
"$v  " \
"$l$h$tr" \
"$v $v" \
"$bl$h$br"

    set -A n7 -- \
"$h$h$tr" \
"  $v" \
"  $v" \
"  $v" \
"  $v"

    set -A n8 -- \
"$tl$h$tr" \
"$v $v" \
"$l$h$r" \
"$v $v" \
"$bl$h$br"

    set -A n9 -- \
"$tl$h$tr" \
"$v $v" \
"$bl$h$r" \
"  $v" \
"  $v"

    set -A nb -- \
"   " \
"   " \
"   " \
"   " \
"   "
	;;
ibm1_5w|ibm2_5w)
    digheight=5 digwidth=9 gap=' '
    set -A n0 -- \
"$tl$h$h$tr" \
"$v  $v" \
"$v  $v" \
"$v  $v" \
"$bl$h$h$br"

    set -A n1 -- \
" $tr  " \
" $v  " \
" $v  " \
" $v  " \
" $b  "

    set -A n2 -- \
" $h$h$tr" \
"   $v" \
"$tl$h$h$br" \
"$v   " \
"$bl$h$h "

    set -A n3 -- \
" $h$h$tr" \
"   $v" \
" $h$h$r" \
"   $v" \
" $h$h$br"

    set -A n4 -- \
"$v  $v" \
"$v  $v" \
"$bl$h$h$r" \
"   $v" \
"   $v"

    set -A n5 -- \
"$tl$h$h " \
"$v   " \
"$bl$h$h$tr" \
"   $v" \
" $h$h$br"

    set -A n6 -- \
"$tl$h$h " \
"$v   " \
"$l$h$h$tr" \
"$v  $v" \
"$bl$h$h$br"

    set -A n7 -- \
"$h$h$h$tr" \
"   $v" \
"   $v" \
"   $v" \
"   $v"

    set -A n8 -- \
"$tl$h$h$tr" \
"$v  $v" \
"$l$h$h$r" \
"$v  $v" \
"$bl$h$h$br"

    set -A n9 -- \
"$tl$h$h$tr" \
"$v  $v" \
"$bl$h$h$r" \
"   $v" \
"   $v"

    set -A nb -- \
"    " \
"    " \
"    " \
"    " \
"    "
    ;;
*)
    echo "$lname: bad value for digits: $digits.  Exiting."
    exit 1
    ;;
esac

# Select box drawing characters.
# If default (ascii) selected, chars are defaults set for ascii
# before argument processing, unless modified during arg processing
# by setting of t, b, etc. on command line.
case $box in
ascii)
    ;;
ibm2)
    t="\0313" b="\0312" l="\0314" r="\0271" tl="\0311" bl="\0310"
    tr="\0273" br="\0274" m="\0316" h="\0315" v="\0272"
    ;;
ibm1) 
    t="\0302" b="\0301" l="\0303" r="\0264" tl="\0332" bl="\0300"
    tr="\0277" br="\0331" m="\0305" h="\0304" v="\0263"
    ;;
*)
    echo "$lname: bad box font name $box.  Exiting."
    exit 1
    ;;
esac

# printhorbar size leftchar middlechar rightchar horchar
# Print a horizontal bar.
# size is the interior length of each section
# leftchar is the char to start the bar with
# middlechar is the char to join two sections with
# rightchar is the char to end the bar with
# horchar is the char to draw each section with
# 7 sections are drawn.
# Example: the bar /---+---+---+---+---+---+---\ is generated by
# printhorbar 3 / + '\' -
printhorbar() {
    typeset -i i=0
    # generate bar section of specified length
    horbar=
    while [ i -lt $1 ]; do
	horbar=$horbar$5
	let i+=1
    done
    echo -n $2$horbar	# print left char & first section
    i=0
    while [ i -lt 6 ]; do
	echo -n $3$horbar
	let i+=1
    done
    echo $4
}

# printver size sepchar datasize [<field1data> [... <field7data>]]
# print a section of a rectangle
# size is the interior (horizontal) length of each section
# sepchar is the char to separate each section with
# datasize is the length each data field will be after escape sequences
# in them are resolved by "echo"
# If it is 0, data are of various lengths, but there are no escape sequences
# so data fields can be forced to a specific length
printver() {
    $debug && print -ru2 -- "printver $*"
    sep=$2
    if [ $3 = 0 ]; then
	# set field var to length size+length(sep)
	typeset -L$(($1 + ${#sep})) field
    else
	# set blank padding to length size-datasize
	typeset -L$(($1 - $3)) blanks
    fi
    # size typeset does not have effect until assignment to var is made
    blanks=
    shift 3
    typeset -i i=0
    while [ i -lt 7 ]; do
	field="$sep$1"
	echo -n "$field$blanks"
	let i+=1
	[ $# -gt 0 ] && shift
    done
    echo $sep
}

# if -f given, read data for fields into d[] from stdin
typeset -i i=1
if $ReadFields; then
    while read d[i] && [ i -lt 32 ]; do 
	let i+=1; 
    done
fi

# run cal & put its output on coprocess descriptor
cal $month |&

# read month & year header from calendar
read -p header
if [[ "$header" = cal:* ]]; then
    echo "Bad month specified."
    exit 1
fi

typeset -i width="(hsize+1)*7+1"
# print top bar of calendar
if $short; then
    printhorbar $hsize $tl $t $tr $h | read line
    typeset -i i="(width-${#header})/2"
    while [ i -gt 0 ]; do
	nline=${line#?}
	print -n -- "${line%$nline}"
	line=$nline
	let i-=1
    done
    while [ -n "$header" ]; do
	nline=${header#?}
	print -n -- "${header%$nline}"
	header=$nline
	line=${line#?}
	let i-=1
    done
    print -- "$line"
else
    # set header to be right-justified with size such that it
    # is printed at the middle of the big calendar
    typeset -R$(( (width + ${#header}) / 2 )) header
    echo "$header"
    printhorbar $hsize $tl $t $tr $h
fi

# read & discard weekday header
read -p dateline

# Print weekday header.  If interior size is less than 9 
# (in which case at least Wednesday would not completely fit),
# abbreviate all days to 3-letter prefix
set -A days Sunday Monday Tuesday Wednesday Thursday Friday Saturday
[ hsize -lt 9 ] && typeset -L3 days
printver $hsize $v 0 ${days[*]}

i=0
while read -p dateline; do
    $debug && print -ru2 "Read from date: $dateline"
    # discard blank line printed by cal
    [ -z "$dateline" ] && break

    # count numbers in this row.  If less than 7 and this is the first
    # row of numbers, set nulldates to a number of null arguments
    # equal to the difference, so that when dateline is prefixed with
    # them and converted to array the numbers will be in the correct
    # array position rather than elements 0, 1, etc.
    set $dateline
    unset nulldates
    if [ $i = 0 -a $# -lt 7 ]; then
	typeset -L$(( (7 - $#) * 3 )) nulldates
	nulldates='"" "" "" "" "" "" "" '
    fi
    eval set -A dateline $nulldates $dateline

    # for each day of the week, use the 0, 1 or 2 digits of the date given
    # by cal to create the digit array for the week in the chosen font.
    # nb[] (the blank digit) is used if there are less than two digits.
    # arrays wdig1n and wdig2n are the first and second digits of the date
    # for weekday n, where n is 0-6 for Sunday-Saturday.
    i=0
    IFS=:
    while [ i -lt 7 ]; do
	dt=${dateline[i]}
	case ${#dt} in
	0) 
	    eval set -A wdig1$i '${nb[@]}'
	    eval set -A wdig2$i '${nb[@]}';;
	1)
	    eval set -A wdig1$i -- '${n'$dt'[@]}'
	    eval set -A wdig2$i '${nb[@]}';;
	2)
	    eval set -A wdig1$i -- '${n'${dt%?}'[@]}'
	    eval set -A wdig2$i -- '${n'${dt#?}'[@]}';;
	esac
	$debug && eval print -ru2 -- \
	\""dt=\$dt  wdig1$i=\${wdig1$i[*]}  wdig2$i=\${wdig2$i[*]}"\"
	let i+=1
    done

    # print horizontal bar to separate calendar rectangles
    printhorbar $hsize $l $m $r $h

    # for each row of characters that digits are constructed from,
    # print a row of each character separated by $gap.
    # Each pair of char rows and gap becomes a data argument to printver.
    i=0
    while [ i -lt digheight ]; do
	printver $hsize $v $digwidth \
	"${wdig10[i]}$gap${wdig20[i]}" \
	"${wdig11[i]}$gap${wdig21[i]}" \
	"${wdig12[i]}$gap${wdig22[i]}" \
	"${wdig13[i]}$gap${wdig23[i]}" \
	"${wdig14[i]}$gap${wdig24[i]}" \
	"${wdig15[i]}$gap${wdig25[i]}" \
	"${wdig16[i]}$gap${wdig26[i]}"
	let i+=1
    done

    # Print field data & finish drawing vertical lines of boxes.
    # o[] has one element for each day of the week.
    # Each element holds the date for that day of the week.
    # The date is used as an index into d[], which holds the field data
    # for each day of the month.
    # For each line to be printed:
    # * The first word of the data for each day of the week is sent to 
    #   printver().  
    # * That word is removed from the data.
    IFS=" "
    set -A o "${dateline[@]}"
    while [ i -lt vsize ]; do
	# eval expands the ${o[n]}'s into real indexes;
	# then ${d[n]%% *} removes all but the first word
	eval printver $hsize '$v' 0 \
	\"${d[${o[0]}]%% *}\" \"${d[${o[1]}]%% *}\" \
	\"${d[${o[2]}]%% *}\" \"${d[${o[3]}]%% *}\" \
	\"${d[${o[4]}]%% *}\" \"${d[${o[5]}]%% *}\" \
	\"${d[${o[6]}]%% *}\"
	# eval expands the ${o[n]}'s into real indexes;
	# then ${d[n]##*([! ])?( )} removes the leftmost word,
	# identified as a string of non-spaces followed by an optional
	# space (optional because the last word may not be followed by
	# a space)
	eval d[${o[0]}]=\"${d[${o[0]}]##*([! ])?( )}\" \
	d[${o[1]}]=\"${d[${o[1]}]##*([! ])?( )}\" \
	d[${o[2]}]=\"${d[${o[2]}]##*([! ])?( )}\" \
	d[${o[3]}]=\"${d[${o[3]}]##*([! ])?( )}\" \
	d[${o[4]}]=\"${d[${o[4]}]##*([! ])?( )}\" \
	d[${o[5]}]=\"${d[${o[5]}]##*([! ])?( )}\" \
	d[${o[6]}]=\"${d[${o[6]}]##*([! ])?( )}\"
	let i+=1
    done
done
# print bottom of calendar
printhorbar $hsize $bl $b $br $h
