#!/packages/bin/perl -w

require 5.008;

# Set DEBUG equal to 1 to print debugging information
local ($DEBUG) = 0;
if ($DEBUG == 1) {
  $| = 1;                                       # Force prints when debugging
  use Data::Dumper;                             # Bring in the data dumper also
}

local ($VERSION) = "3.0.0";			# Current version
local ($COPYRIGHT) = "
Copyright (c) 1998-2005 Xerox Corporation.  All Rights Reserved.

Permission to use, copy, modify  and  distribute  without  charge
this  software,  documentation, images, etc. is granted, provided
that this copyright and the author's name is retained.
    
A fee may be charged for this program ONLY to recover  costs  for
distribution  (i.e.  media costs).  No profit can be made on this
program.
        
The author assumes no responsibility for  disasters  (natural  or
otherwise) as a consequence of use of this software.
            
Adam Stein (adam\@scan.mc.xerox.com)
";

require "hostname.pl";

use Date::Calc qw(:all);
use Getopt::Std;
use Locale::Hebrew::Calendar qw(g2j j2g);

# Figure out if we can deal with vCalendar format
local ($have_vCalendar);
eval 'use DateTime ; use iCal::Parser';

if ($@) {
  $have_vCalendar = 0;
} else {
  $have_vCalendar = 1;
}

use vars qw($COPYRIGHT $VERSION $opt_v);

# Calendar appointment formats
use constant F_VCALENDAR => 1;			# vCalendar (ICS) format
use constant F_CDE => 2;			# CDE Calendar Manager format
use constant F_OW => 3;				# OW Calendar Manager format

# Calendar managers
use constant CDECM => "dtcm_lookup";		# CDE calendar manager
use constant OWCM  => "cm_lookup";		# OW calendar manager

use constant MAXCHAR => 32;			# Max chars that can fit in
						# calendar day box

# Global variables
local ($program);				# Name of current program

MAIN:
{
  my (%appts),					# Appointments to list in cal
  my (%opts),					# Command line option info
  my ($ib4e),					# Include before Exclude flag
  my ($month),					# Month of calendar
  my (%rcinfo),					# Information from RC file
  my ($year);					# Year of calendar

  # Set name of current program
  ($program = $0) =~ s#.*/##;

  # Parse command line arguments
  &ParseArgs(\%opts);

  # Get the month and year (from command line or current values)
  if (!defined($opts{"d"})) {
    ($year, $month) = Today
  } else {
    ($month, $year) = split(/\s+/, $opts{"d"});
    ($year) = This_Year if (!defined($year)); # Set to current year if not set
  }

  # Need to convert the month to a numeric value if not already
  if ($month !~ /[0-9]+/) {
    if (Decode_Month($month)) {
      $month = Decode_Month($month)
    } else {
      die "$program: unrecognized month --> '$month'\n";
    }
  }

  print "Using Month = <$month>, Year = <$year>\n\n" if ($DEBUG);

  # Read in additional information if available
  $ib4e = &ReadRCFile($month, $year, \%rcinfo)
  	if (-f "$ENV{'HOME'}/.printcalrc");

  # Read in additional calendar files if available
  if (defined($$opts{"c"})) {
    foreach $calfile (split(/\s/, $$opts{"c"})) {
      print STDERR "Reading in <$calfile>\n" if ($DEBUG);
      &ReadCalFile($calfile, \$rcinfo{"holiday"});
    }
  }

  # Get calendar appointments
  &GetAppts(\%opts, \%rcinfo, $month, $year, $ib4e, \%appts);

  # Run the PS calendar program
  &PrintCal(\%appts, \%rcinfo, \%opts, $month, $year);
}

# Parse command line arguments
sub ParseArgs {
  my ($opts) = shift;				# Parsed options

  # a = pcal arguments
  # c = additional calendar files
  # d = date for calendar
  # f = calendar format to retrieve appointments from
  # h = help
  # i = vCalendar (ICS) file
  # m = calendar manager host
  # n = use no additional calendar files
  # o = filename to save PostScript calendar to
  # p = print PostScript output
  # P = printer queue name
  # u = user name
  # v = show version and exit
  getopts("a:c:d:f:hi:m:no:pP:u:v", $opts);

  # Print version and exit if requested
  if ($$opts{"v"}) {
    print "$program: v$VERSION\n";
    exit(0);
  }

  # Show help and exit if requested
  if ($$opts{"h"}) {
    &ShowHelp;
    exit(0);
  }

  # Command line option error checking

  # Check specified format
  die "$program: valid formats (-f) are 'vcal', 'cde', or 'ow'\n"
  	if (defined($$opts{"f"}) && ($$opts{"f"} !~ /^vcal/) &&
	    ($$opts{"f"} ne "cde") && ($$opts{"f"} ne "ow"));

  # Error if we have listed calendar files and the "no calendar" option
  die "$program: -c and -n options are mutally exclusive, can't use both\n"
	if (defined($$opts{"c"}) && defined($$opts{"n"}));

  # Error if not specified what to do with the output
  die "$program: not specified what to do with the PostScript output (-o or -p)\n"
  	if (!defined($$opts{"o"}) && !defined($$opts{"p"}));
}

# Show help for this program
sub ShowHelp {
  print "usage: $program [-a args] [-c filename(s)] [-d 'month [year]'] [-f format] [-h] [-i filename] [-m host] [-n] [-o filename] [-p] [-P queue] [-u name] [-v]\n";

  print "\n";
  print "  -a = arguments to pass to the 'pcal' program\n";
  print "  -c = additional calendar files to merge in\n";
  print "  -d = date to generate the calendar for\n";
  print "  -f = input desktop calendar format (vcal, cde, or ow)\n";
  print "  -h = show this help\n";
  print "  -i = input desktop calendar vCalendar format file\n";
  print "  -m = calendar manager host (when using cde or ow)\n";
  print "  -n = do not use any additional calendar files\n";
  print "  -o = filename to save PostScript output to\n";
  print "  -p = print PostScript output\n";
  print "  -P = printer queue to send PostScript output to (if using -p)\n";
  print "  -u = user name (when using cde or ow)\n";
  print "  -v = show version and exit\n";
}

# Parse the RC file
sub ReadRCFile {
  my ($nmonth) = shift,				# Numerical month
  my ($year) = shift,				# [Month] Year of calendar
  my ($rcinfo) = shift,				# Information from RC file
  my (@date),					# Date of holiday (in words)
  my ($day),					# Day
  my ($desc),					# Description of holiday
  my ($dow),					# Day of the week
  my ($ib4e),					# Include before Exclude flag
  my ($list),					# List of includes or excludes
  my ($month);					# Month

  $$rcinfo{"include"} = ();
  $$rcinfo{"exclude"} = ();
  $$rcinfo{"holiday"} = {};

  $ib4e = -1;

  open(RCFILE,"$ENV{'HOME'}/.printcalrc") ||
  	die "$program: can't open '$ENV{'HOME'}/.printcalrc'\n";

  # Loop thru lines in file
  while (<RCFILE>) {
    chomp;

    # Ignore comment and blank lines
    next if (/^#/ || !length);

    # Save user name
    if (/^User:\s*(.*)/i) {
      $$rcinfo{"user"} = $1;
      print STDERR "User Name = <", $$rcinfo{"user"}, ">\n" if ($DEBUG);
    }

    # Save calendar server hostname
    if (/^Host:\s*(.*)/i) {
      $$rcinfo{"host"} = $1;
      print STDERR "Host Name = <", $$rcinfo{"host"}, ">\n" if ($DEBUG);
    }

    # Save calendar format
    if (/^Format:\s*(.*)/i) {
      $$rcinfo{"format"} = $1;
      print STDERR "Format = <", $$rcinfo{"format"}, ">\n" if ($DEBUG);

      # Check specified format
      die "$program: valid formats (Format: in RC file) are 'vcal', 'cde', ",
          "or 'ow'\n"
  	if (($$rcinfo{"format"} !~ /^vcal/) &&
	    ($$rcinfo{"format"} ne "cde") &&
	    ($$rcinfo{"format"} ne "ow"));
    }

    # Save holiday information
    if (/^Holiday:\s*([0-9]*:\s*[0-9]*):(.*)/i) {
      # Make sure date is good
      ($month, $day) = split(":", $1);

      if (check_date($year, $month, $day) == 0) {
        warn "$program: line $.: the date specified:\n\n",
	     " " x (length($program) + 2), "\"$1\"\n\n",
	     " " x (length($program) + 2),
	     "is not a valid date ... skipping\n\n";

	next;
      }

      &AddHoliday($$rcinfo{"holiday"}, $1, $2);
    } elsif (/^Holiday:\s*([0-9]*):([^:]*):(.*)/i) {
      @date = split(" ", $2);
      $month = $1;
      $desc = $3;


      # If we split into only 2 pieces, then we have the form:
      #
      #		nth day
      #
      # otherwise, if we have 5 pieces, we have the form:
      #
      #		nth day [after|before] nth day
      if (scalar(@date) == 2) {
        # Check nth term for validity
	$nth1 = &CheckNth($date[0]);
	next if (($nth1 ne "last") && ($nth1 == -1));

	# Check weekday term for validity
	next if (($dow = &CheckDOW($date[1])) == -1);

	# Figure out the day of the month
	$day = &GetDay($year, $month, $dow, $nth1);

	# Save holiday line
	&AddHoliday($$rcinfo{"holiday"}, "$month:$day", $desc);
      } elsif (scalar(@date) == 5) {
	# Get day of 2nd date specified, then get day of 1st date
	# specified which is relative to the 2nd date (either before
	# or after)

	# Check 2nd nth term for validity
	next if (($nth2 = &CheckNth($date[3])) == -1);

	# Additional check for 2nd nth term.  It can be the term "last"
	# ONLY if the relative keyword is "before".  Otherwise it is
	# not needed
	if (($nth2 eq "last") && ($date[2] !~ /before/i)) {
	  warn "$program: line $.: can't use \"last\" as 2nd Nth term in this context ... skipping\n";
	  next;
	}

	# Check 2nd weekday term for validity
	next if (($dow = &CheckDOW($date[4])) == -1);

	# Figure out the day of the month
	$day = &GetDay($year, $month, $dow, $nth2);

	# Figure out the 1st occurence of the day of the 1st date
	# AFTER the 2nd (we will compensate for *before* later)
	if ((Decode_Day_of_Week($date[1]) - $dow) < 0) {
	  $diff = 7 - ($dow - Decode_Day_of_Week($date[1]));
	} else {
	  $diff = Decode_Day_of_Week($date[1]) - $dow;
	}
	$day += $diff;

	# Check 1st nth term for validity
	next if (($nth1 = &CheckNth($date[0])) == -1);

	# Additional check for 2nd nth term.  It can never be "last".
	if ($nth1 eq "last") {
	  warn "$program: line $.: can't use \"last\" as 1st Nth term ... skipping\n";
	  next;
	}

	# Figure out the day of the 1st date relative to the 2nd
	$date[2] = lc($date[2]);

	if ($date[2] eq "after") {
	  for ($loop = $nth1;$loop > 1;--$loop) {
	    $day += 7;
	  }
	} elsif ($date[2] eq "before") {
	  for ($loop = $nth1;$loop > 0;--$loop) {
	    $day -= 7;
	  }
	} else {
	  warn "$program: line $.: don't understand relative word --> $date[2], skipping\n";
	  next;
	}

	# Make sure date is good
	if (check_date($year, $month, $day) == 0) {
	  warn "$program: line $.: the date specified:\n\n",
	       " " x (length($program) + 2), "\"$2\"\n\n",
	       " " x (length($program) + 2),
	       "is not a valid date ... skipping\n\n";

	  next;
	}

	# Save holiday line
	&AddHoliday($$rcinfo{"holiday"}, "$month:$day", $desc);
      } else {
        warn "$program: line $.: can't figure out the day from --> \"$2\", skipping\n";
        next;
      }
    } elsif (/^Holiday:\s*([a-zA-Z]*):(.*)?/i) {
      my ($add) = 0,
      my ($event) = $1;

      $desc = (length($2)) ? $2 : $1;

      # Figure out which holiday to get the date for
      # (even if they are not for the month we're
      #  doing, pcal will handle it NOT being seen)
      if ($event =~ /Easter/i) {
        ($year, $month, $day) = Easter_Sunday($year);

	$add = 1;
      } elsif ($event =~ /[Cc]?[HhKk]an{1,2}uk{1,2}ah?/i) {
        my ($jday),
	my ($jmonth),
	my ($jyear),
	my ($year2);

	# Convert the 1st of this month to the Hebrew calendar
	# so that we can get the Hebrew year
	($jday, $jmonth, $jyear) = g2j(1, $nmonth, $year);

	# If we are past Kislev, go to the next year
	++$jyear if ($jmonth > 3);

	# Convert back given the correct Jewish year
	# to get Hanukkah's date
	($day, $month, $year2) = j2g(25, 3, $jyear);

	++$month;	# j2g doesn't return the correct month it seems

	$add = 1;
      }

      # Save holiday line
      &AddHoliday($$rcinfo{"holiday"}, "$month:$day", $desc) if ($add);
    }

    # Save inclusion list
    if (/^Include:\s*(.*)/i) {
      $ib4e = 1 if ($ib4e == -1);
      $list = $1; chomp($list);

      foreach (split(/\s/, $list)) {
        push(@{$$rcinfo{"include"}}, $_);
      }
    }

    # Save exclusion list
    if (/^Exclude:\s*(.*)/i) {
      $ib4e = 0 if ($ib4e == -1);
      $list = $1; chomp($list);

      foreach (split(/\s/, $list)) {
        push(@{$$rcinfo{"exclude"}}, $_);
      }
    }

    # Save vCalendar (ICS) format file
    if (/^vCalendar:\s*(.*)/i) {
      $$rcinfo{"vcal"} = $1;
      print STDERR "vCalendar = <", $$rcinfo{"vcal"}, ">\n" if ($DEBUG);
    }
  }

  close(RCFILE);

  return($ib4e);
}

# Add holiday to the list
sub AddHoliday {
  my ($list) = shift,				# Holiday list
  my ($date) = shift,				# Holiday date to save
  my ($holiday) = shift;			# Holiday to save

  $$list{$date} = [] if (!defined($$list{$date}));

  push(@{$$list{$date}}, $holiday);
  print STDERR "Added Holiday {$date} = <$holiday>\n" if ($DEBUG);
}

# Check the Nth term of a date
sub CheckNth {
  my ($nth) = shift;				# Nth term to check

  if ($nth =~ /^(?:[1-5]|1st|2nd|3rd|4th|5th)$/) {
    return(substr($nth, 0, 1));
  } elsif ($nth =~ /^last$/i) {
    return("last");
  } else {
    warn "$program: line $.: \"$nth\" must be 1..5 or \"1st\", \"2nd\", \"3rd\", \"4th\" or \" 5th\", skipping\n";
    
    return(-1);
  }
}

# Check the Day Of the Week
sub CheckDOW {
  my ($dow) = shift,				# Weekday term to check
  my ($origdow);				# Original value

  $origdow = $dow;

  unless ($dow =~ /^\d+$/) { $dow = Decode_Day_of_Week($dow); }

  if (($dow < 1) || ($dow > 7)) {
    warn "$program: line $.: \"$origdow\" must be 1..12 or name of month in English, skipping\n";
    
    return(-1);
  } else {
    return($dow);
  }
}

# Figure out the day of the month
sub GetDay {
  my ($year) = shift,				# Year of date
  my ($month) = shift,				# Month of date
  my ($dow) = shift,				# Day of week
  my ($nth) = shift,				# "Nth" day of month
  my ($rday),					# Day of resultant date
  my ($status);					# Status of getting nth day

  if ($nth eq "last") {
    $nth = 5;

    # Keep checking until we find the last day (start at 5th, then 4th)
    while (! defined($rday)) {
      &GetNthDay($year, $month, $dow, $nth, \$rday);
      --$nth;
    }
  } else {
    $status = &GetNthDay($year, $month, $dow, $nth, \$rday);

    if ($status) {
      if ($status =~ /^(.+?)\s*at\s/) { die "$1!\n"; }
      else                            { die $status; }
    }
  }

  return($rday);
}

# Return the Nth day
sub GetNthDay {
  my ($year) = shift,				# Year of date
  my ($month) = shift,				# Month of date
  my ($dow) = shift,				# Day of week
  my ($nth) = shift,				# "Nth" day of month
  my ($rday) = shift,				# Day of resultant date
  my ($rmonth),					# Month of resultant date
  my ($ryear);					# Year of resultant date

  eval {
    ($ryear, $rmonth, $$rday) = Nth_Weekday_of_Month_Year($year, $month,
							  $dow, $nth);
  };

  return($@);
}

# Read and parse calendar files
sub ReadCalFile {
  my ($file) = shift,				# Calendar file to read
  my ($holiday) = shift;			# Holiday list

  open(CALFILE, $file) || die "$program: can't open '$file'\n";

  # Parse calendar file
  while (<CALFILE>) {
    chomp;

    # Parse valid lines and store the events found
    if (m#([0-9]*)/([0-9]*)\s*(.*)#) {
      &AddHoliday($holiday, "$1:$2", $3);
    } else {
      warn "$program: don't understand line $. in $file ... skipping\n";
    }
  }

  close(CALFILE);
}

# Fetch the calendar appointments
sub GetAppts {
  my ($opts) = shift,				# Parsed command line options
  my ($rcinfo) = shift,				# Information from RC file
  my ($month) = shift,				# Month of calendar
  my ($year) = shift,				# Year of calendar
  my ($ib4e) = shift,				# Include before Exclude flag
  my ($appts) = shift,				# Appointments to list in cal
  my ($cm),					# Calendar manager program
  my ($cmd),					# Command to run for CM server
  my ($date),					# Date to use for calendar prog
  my ($host),					# Calendar manager host machine
  my ($format),					# Input calendar format
  my ($label),					# Label for error message`
  my ($user);					# Calendar account user name

  # Figure out what format we have to deal with
  $format = $$opts{"f"} if (defined($$opts{"f"}));
  $format = $$rcinfo{"format"} if (!defined($format));

  if (!defined($format) || ($format =~ /^vcal/)) {
    if ($have_vCalendar) {
      &GetAppts_vCalendar($opts, $rcinfo, $month, $year, $ib4e, $appts);
    } else {
      die "$program: the necessary Perl modules for vCalendar ",
     	  "support are NOT installed\n";
    }
  } elsif (($format eq "cde") || ($format eq "ow")) {
    if ($format eq "cde") {
      $cm = CDECM;
      $label = "CDE";
    } else {
      $cmd = OWCM;
      $label = "OW";
    }

    # Make sure we have the user name and host for the calendar manager.
    # Set user and host to the current user and machine if they haven't
    # been set by either the command line or the RC file
    $user = (defined($$opts{"u"})) ? $$opts{"u"} : $$rcinfo{"user"};
    $user = getlogin if (!defined($user));

    $host = (defined($$opts{"m"})) ? $$opts{"m"} : $$rcinfo{"host"};
    $host = &hostname if (!defined($host));

    # Create the date to pass to the calendar manager
    $date = "$month/01/$year";

    # Put together the command to run the calendar manager
    print "Running <$cmd>\n" if ($DEBUG);
    $cmd = "$cm -c $user\@$host -v month -d $date |";

    if (`which $cm 2>&1` !~ /no $cm in/) {
      &GetAppts_CM($cmd, $cm, $rcinfo, $month, $year, $ib4e, $appts);
    } else {
      die "$program: '$cm' $label calendar manager not found\n";
    }
  }
}

# Fetch appointments from a file in vCalendar format
sub GetAppts_vCalendar {
  my ($opts) = shift;                           # Parsed command line options
  my ($rcinfo) = shift,				# Information from RC file
  my ($month) = shift,				# Month of calendar
  my ($year) = shift,				# Year of calendar
  my ($ib4e) = shift,				# Include before Exclude flag
  my ($appts) = shift,				# Appointments to list in cal
  my (@ask),					# Appts to ask the user about
  my ($day),					# Day of the month
  my ($date),					# Date of calendar to get
  my ($desc),					# Description of event
  my ($end),					# Ending time of event
  my ($hash),					# Parsed vCalendar file
  my ($ics),					# vCalendar (ICS) file to parse
  my ($idesc),					# Index of desc in array
  my ($parser),					# vCalendar format parser
  my ($start),					# Starting time of event
  my ($ts);					# Timestamp of specific event

  # Make sure we were given the filename to parse
  $ics = $$opts{"i"};				# Overrides RC file
  $ics = $$rcinfo{"vcal"} if (!defined($ics));

  die "$program: no vCalendar (ICS) file given\n" if (!defined($ics));
  (-f $ics) || die "$program: can't find vCalendar file, '$ics'\n";

  # Turn the date into the format needed for the parser
  $date = sprintf("%04d%02d01", $year, $month);

  $parser = iCal::Parser->new("start" => $date, "months" => 1);
  $hash = $parser->parse($ics);

  # Go thru appointments for each day in the desired month
  foreach $day (sort {$a <=> $b} keys(%{$$hash{"events"}->{$year}->{$month}})) {
    undef(@ask);

    foreach $ts (sort keys(%{$$hash{"events"}->{$year}->{$month}->{$day}})) {
      $start = $$hash{"events"}->{$year}->{$month}->{$day}->{$ts}->{"DTSTART"};
      $end = $$hash{"events"}->{$year}->{$month}->{$day}->{$ts}->{"DTEND"};

      # Add starting time, ending time, and event description to array
      # so that we can strip off the ending time for the final calendar
      # easily
      $idesc = 0;
      $desc = [];
      if (!$$hash{"events"}->{$year}->{$month}->{$day}->{$ts}->{"allday"}) {
        push(@$desc, $start->strftime("%I:%M%P"));
	push(@$desc, "-");
	push(@$desc, $end->strftime("%I:%M%P") . " ");

	$idesc = 3;
      }
      push(@$desc, $$hash{"events"}->{$year}->{$month}->{$day}->{$ts}->{"SUMMARY"});

      # Handle the event
      &HandleEvent($day, $desc, $idesc, $rcinfo, $ib4e, $appts, \@ask);
    }
    
    # Ask the user about any appointments that aren't automatically
    # included or excluded
    &QueryUser(\@ask, $month, $day, $year, $appts) if (scalar(@ask));
  }
}

# Fetch appointments from the CDE or OW calendar manager
sub GetAppts_CM {
  my ($cmd) = shift,				# Command to run for cal server
  my ($cm) = shift,				# Calendar manager program
  my ($rcinfo) = shift,				# Information from RC file
  my ($month) = shift,				# Month of calendar
  my ($year) = shift,				# Year of calendar
  my ($ib4e) = shift,				# Include before Exclude flag
  my ($appts) = shift,				# Appointments to list in cal
  my (@ask),					# Appts to ask the user about
  my ($day),					# Day of the month
  my ($desc),					# Description of event
  my ($end),					# Ending time of event
  my ($idesc),					# Index of desc in array
  my ($line),					# Current line
  my ($numblank),				# # of sequential blank lines
  my (@piece),					# Pieces of an appointment line
  my ($start);					# Starting time of event

  $numblank = 0;

  # Parse the appointments from the calendar program
  open(CAL, $cmd) || die "$program: execution of '$cm' failed\n";

  # Go thru appointments for each day in the desired month
  while (<CAL>) {
    $line = $_;
    
    # Blank lines mean an event is done
    if (/^$/) {
      ++$numblank;

      # Handle the event when we hit the first blank line
      &HandleEvent($day, $desc, $idesc, $rcinfo, $ib4e, $appts, \@ask)
      	if (defined($day) && ($numblank == 1));

      next;
    }

    $numblank = 0;	# Reset if no blank line
    
    if (/^Appointments/) {
      # Finish up the previous day if there was a previous day
      if (defined($day)) {
        # Ask the user about any appointments that aren't automatically
        # included or excluded
        &QueryUser(\@ask, $month, $day, $year, $appts) if (scalar(@ask));

	undef(@ask);
      }

      # Get the day of the appointment
      ($day = $line) =~ s/Appointments for [^0-9]+([0-9]+).*/$1/;
      chomp($day);
    } elsif (/^\s+[0-9]+\)/) {
      # Beginning of an appointment (save only the first line)

      # Set up so that we can do the next step
      $line =~ s/\s*[0-9]+\)\s*//;	# Remove leading stuff for split()
      @piece = split(/\s/, $line);	# Easier to get to the pieces we want
      
      # Add starting time, ending time, and event description to array
      # so that we can strip off the ending time for the final calendar
      # easily
      $idesc = 0;
      $desc = [];
      if ($piece[0] =~ /^[0-9]/) {
        ($start = $piece[0]) =~ s/-.*//;
	$start = "0$start" if (length($start) == 6);
        ($end = $piece[0]) =~ s/.*-//;
	$end = "0$end" if (length($end) == 6);
	
	push(@$desc, $start);
	push(@$desc, "-");
	push(@$desc, $end . " ");
	
	$idesc = 3;

	# Put together all the pieces of the event description
	my (@slice) = splice(@piece, 1);
	push(@$desc, join(" ", @slice));
      } else {
	# Put together all the pieces of the event description
        push(@$desc, join(" ", @piece));
      }
    }
  }

  # Ask the user about any appointments that aren't automatically
  # included or excluded.  We need to do this one last time to
  # catch the last appointment.
  &QueryUser(\@ask, $month, $day, $year, $appts) if (scalar(@ask));

  close(CAL);
}

# Handle event
sub HandleEvent {
  my ($day) = shift,				# Day of the month
  my ($desc) = shift,				# Description of event
  my ($idesc) = shift,				# Index of desc in array
  my ($rcinfo) = shift,				# Information from RC file
  my ($ib4e) = shift,				# Include before Exclude flag
  my ($appts) = shift,				# Appointments to list in cal
  my ($ask) = shift;				# Appts to ask user about

  print "Evaluating <$$desc[$idesc]> ... " if ($DEBUG == 1);

  # When dealing with an appointment, include it automatically if a keyword
  # from the inclusion list is found, exclude automatically if a keyword from
  # the exclusion list, or add it to the list of appointments to ask the user
  # what to do with.  We check inclusion or exclusion first depending on which
  # is first in the RC file.
  if (($ib4e || !grep($$desc[$idesc] =~ /$_/i, @{$$rcinfo{"exclude"}})) &&
      grep($$desc[$idesc] =~ /$_/i, @{$$rcinfo{"include"}})) {
    # Include automatically

    # Append starting time
    $$desc[$idesc] = $$desc[0] . " $$desc[$idesc]" if ($idesc);

    $$appts{$day} .= "$$desc[$idesc]|";

    print "included\n" if ($DEBUG == 1);
  } elsif (!grep($$desc[$idesc] =~ /$_/i, @{$$rcinfo{"exclude"}})) {
    # Ask user what to do
    push(@$ask, $desc);

    print "ask user\n" if ($DEBUG == 1);
  } else {
    print "excluded\n" if ($DEBUG == 1);
  }
}

{
my ($first);                              	# Flag indicating 1st appt

BEGIN {$first = 1}				# Initialize our 'static' var

# Query the user about appointsment we don't know how to handle
sub QueryUser {
  my ($ask) = shift,				# Appts to ask user about
  my ($month) = shift,				# Month of calendar
  my ($day) = shift,				# Day of appointments
  my ($year) = shift,				# Year of calendar
  my ($appts) = shift,				# Appointments to list in cal
  my ($answer),					# User input
  my ($indx),					# Index of appts to ask array
  my ($numappts);				# Number of appts to ask about
  
  # Ask the user about any appointments that aren't automatically
  # included or excluded
  if (scalar(@$ask)) {
    # List the appts
    if ($first) {
      $first = 0;
    } else {
      print "\n", '-' x 64, "\n\n";
    }
    print "Appointments for ", Date_to_Text_Long($year, $month, $day), ":\n";

    for $indx (1..scalar(@$ask)) {
      print "          $indx) " . join("", @{$$ask[$indx - 1]}) . "\n";
    }
    print "\n";

    # Get user input to keep some, all, or none of the appts.  User
    # input is bad if:
    #
    #	      - a single number answer isn't in the range
    #	      - a multiple number answer has a number not in the range
    #	      - a character answer doesn't equal "a" or "n"
    $numappts = scalar(@$ask);
    do {
      if ($numappts > 1) {
	print "Which appointments should be kept (1-$numappts, a = all, n = none) [n] ? ";
      } else {
	print "Which appointments should be kept (1, a = all, n = none) [n] ? ";
      }

      chomp($answer = <STDIN>);
    } while (
	      # [a]ll or [n]one
	      ($answer ne "a") && ($answer ne "n") &&

	      # single or list of numbes within the valid range
	      &InvalidNumber($answer, $numappts)
	    );

    # Save those appts the user told us to
    if (length($answer)) {
      if (length($answer) > 1) {
	# Saving a non-contiguous list
	foreach (split(/ /, $answer)) {
	  if (scalar(@{$$ask[$_ - 1]}) > 1) {
	    $$appts{$day} .= "$$ask[$_ - 1]->[0] $$ask[$_ - 1]->[3]|";
	  } else {
	    $$appts{$day} .= "$$ask[$_ - 1]->[0]|";
	  }
	}
      } elsif ($answer eq "a") {
        # Saving ALL appts
        foreach (@$ask) {
	  if (scalar(@{$_}) > 1) {
	    $$appts{$day} .= "$$_[0] $$_[3]|";
	  } else {
	    $$appts{$day} .= "$$_[0]|";
	  }
        }
      } elsif ($answer ne "n") {
        # Save 1 appt
        if (scalar(@{$$ask[$answer - 1]}) > 1) {
	  $$appts{$day} .= "$$ask[$answer-1]->[0] $$ask[$answer - 1]->[3]|";
        } else {
	  $$appts{$day} .= "$$ask[$answer - 1]->[0]|";
        }
      }
    }
  }
}

}

# Determine if we have an invalid number
sub InvalidNumber {
  my ($list) = shift,				# List of numbers to check
  my ($top) = shift;				# Top of the range

  # Loop thru and check each number
  foreach (split(/ /, $list)) {
    return(1) if (($_ !~ /[0-9]+$/) || ($_ < 1) || ($_ > $top));
  }

  return(0);
}

# Create the merged calendar in PS format
sub PrintCal {
  my ($appts) = shift,				# Appts to put on calendar
  my ($rcinfo) = shift,				# Information from RC file
  my ($opts) = shift,				# Parsed options
  my ($month) = shift,				# Month of calendar
  my ($year) = shift,				# Year of calendar
  my ($day),					# Day
  my ($event),					# Calendar event
  my (@events),					# All events in a given day
  my ($pcal),					# Version of "pcal" used
  my ($printcmd),				# Print command
  my ($tmpdir);					# Temp directory

  # Figure out what to use for a temp directory
  $tmpdir = (defined($ENV{"TMP"})) ? $ENV{"TMP"} : "/tmp";

  # Create calendar file for "pcal"
  open(CALENDAR, "> $tmpdir/$program.$$") ||
  	die "$program: can't open tmp file, '$tmpdir/$program.$$'\n";

  # Add any holidays from the startup file
  foreach (keys(%{$$rcinfo{"holiday"}})) {
    foreach $day (@{$$rcinfo{"holiday"}{$_}}) {
      # Format date for "pcal"
      s/:/\//; s/\s*//g;

      $day = substr($day, 0, MAXCHAR);		# Cut down to fit day box
      print CALENDAR "$_ $day\n";
    }
  }

  # Figure out the date of the appts (in number form),
  # and then add the appts to the calendar file
  foreach (keys(%$appts)) {
    @events = split(/\|/,$$appts{$_});
    @events = sort SortByTime @events;

    # Write out appointments
    foreach $event (@events) {
      $event = substr($event, 0, MAXCHAR);	# Cut down to fit day box
      print CALENDAR "$month/$_ $event\n";
    }
  }

  close(CALENDAR);

  # Get version number of pcal so we can include it in our PS output
  ($pcal = `pcal -v`) =~ s/ -.*//;
  chomp($pcal);

  # Add the tag line that specifies the versions of printcal and pcal used
  $$opts{"a"} .= " -f $tmpdir/$program.$$ -C 'Created by $program v$VERSION (using $pcal)' ";

  # Add option to pcal for saving to a file if given
  $$opts{"a"} .= "-o $$opts{'o'}" if (defined($$opts{"o"}));

  # Figure out if we are sending the output to the printer
  if ($$opts{"p"}) {
    $printcmd = (defined($$opts{"P"})) ? "| lp -d $$opts{'P'}" : "| lp";
  } else {
    $printcmd = "";
  }

  if (!$DEBUG) {
    # Run the PS calendar program
    system("pcal $$opts{'a'} $month $year $printcmd");

    unlink("$tmpdir/$program.$$");
  } else {
    print "Would normally execute <pcal $$opts{'a'} $month $year $printcmd>\n";
  }
}

# Sort two entries by time.  Non-timed entries sort before those with times.
# If neither have times, sort alphabetically.
sub SortByTime {
  if (($a =~ /^[0-9]/) && ($b =~ /^[0-9]/)) {
    # Both entries have a time

    my ($timeA),				# Time from entry 1
    my (@timeA),				# Hour/Minute breakdown
    my ($timeB),				# Time from entry 2
    my (@timeB),				# Hour/Minute breakdown
    my ($ampmA),				# AM/PM from entry 1
    my ($ampmB),				# AM/PM from entry 2
    my ($minutesA),				# Entry 1 time in minutes
    my ($minutesB);				# Entry 2 time in minutes

    ($timeA) = split(/ /, $a);	# Just want the first split piece
    ($timeB) = split(/ /, $b);

    ($ampmA = $timeA) =~ s/^[^ap]*//;
    ($ampmB = $timeB) =~ s/^[^ap]*//;

    $timeA =~ s/[ap]m//;		# Remove am/pm designation
    @timeA = split(/:/, $timeA);
    $timeB =~ s/[ap]m//;		# Remove am/pm designation
    @timeB = split(/:/, $timeB);

    # Convert to military time to make it easier to convert to seconds
    $timeA[0] += 12 if ($ampmA eq "pm");
    $timeB[0] += 12 if ($ampmB eq "pm");

    # Convert to minutes to make it easier for comparison
    $minutesA = $timeA[0]*60 + $timeA[1];
    $minutesB = $timeB[0]*60 + $timeB[1];

    return($minutesA <=> $minutesB);
  } elsif ($a =~ /^[0-9]/) {
    # Only first entry has a time
    return(-1);
  } elsif ($b =~ /^[0-9]/) {
    # Only second entry has a time
    return(1);
  } else {
    # Neither have a time
    return($a cmp $b);
  }
}

