#!/usr/bin/perl
#
# Calendar 1.5
# Copyright (C) 2008, 2011
# Paul E. Jones <paulej@packetizer.com>
# Free open source software, no restrictions or warranty whatsoever
#
# This file will open the calendar file and display every line that matches
# the current date.  Blank lines or lines starting with '#' are ignored.
# The date may be expressed in any of these forms.  Years must be
# written as 4-digit years in order to allow differentiation of dates.
# Following the date is a message that should be displayed.  What this
# program actually does is display the date a space and the message text.
# The following calendar entry forms are valid:
#
#  # Comment line
#  include <filename of another calendar file to read>
#  yyyy-mm-dd Message to be displayed
#  yyyy/mm/dd Message to be displayed
#  mm-dd-yyyy Message to be displayed
#  mm/dd/yyyy Message to be displayed
#  mm/dd Message to be displayed
#  mm-dd Message to be displayed
#
# Either the '-' or '/' character may be used to separate entries in the
# date field.
#
# Note that the dd/mm/yyyy and yyyy/dd/mm formats are NOT supported, since
# one cannot distinguish some dates from the mm/dd/yyyy or yyyy/dd/mm formats.
#
# A wildcard '*' may be used in place of the numeric entries in order
# to match any day, month, or year.  As examples:
#  *-16-2007 Match the 16th of every month of 2007
#  *-*-* Match every day
#  2007-*-16 Match the 16th of every month of 2007
#  2007-*-* Match every day in 2007
#  2007-06-* Match every day June of 2007
#  *-06-* This is an ambiguous match and will be interpreted yyyy-06-*
# 
# Note that for wildcard year matching, *-06-* is ambiguous.  Should that
# be yyyy-06-* or *-06-yyyy (i.e., yyyy-mm-dd or mm-dd-yyyy)?
# This program assumes a yyyy-mm-dd format in such ambiguous cases.
#
# The default name for the calendar file is "$HOME/calendar/calendar"
#
# The program accepts one command-line argument, which is a timestamp
# to the date that the user would like to use as calendar execute
# (versus today's date).  The timestamp that may be provided must be
# in seconds since Jan 1, 1970.  That is, the value returned by
# that time() API call on most operating systems.  A separate program
# called daily_calendar can be used to run the calendar program repeatedly
# for each day since the last run, which is useful when a machine has
# been shut down for a period of time.
#

use strict;

#
# Process a calendar file
#
# Note that the calendars array and print_header scalar are passed by
# reference and are modified by this function.
#
sub process_calendar
{
    my ($calendar_file,
        $calendars,
        $print_header,
        $mon,
        $mday,
        $year) = @_;

    my ($date,
        @date,
        $junk,
        $message,
        $ignore_calendar,
        $command,
        $i,
        $index);

    open(CAL, "< $calendar_file") ||
        die "Could not open calendar file\n";

    while(<CAL>)
    {
        next if /^#/;
        next if /^[ \t]*$/;
        if (/^include[ \t]/)
        {
            ($command, $junk, $calendar_file) = split(/( |\t)+/, $_, 2);
            chomp($calendar_file);
            next unless length($calendar_file) > 0;
            # Check to ensure that any given calendar file is specified
            # for processing no more than one time.  Ignore requests for
            # duplicate processing.
            $ignore_calendar = 0;
            for($i = 0; $i <= $#{$calendars}; $i++)
            {
                if ($calendar_file eq ${$calendars}[$i])
                {
                    $ignore_calendar = 1;
                    last;
                }
            }
            if ($ignore_calendar == 0)
            {
                push(@{$calendars}, $calendar_file);
            }
            else
            {
                print "Warning: $calendar_file referenced more than once\n";
            }
            next;
        }
        ($date, $junk, $message) = split(/( |\t)+/, $_, 2);
        @date = split(/(-|\/)/, $date, 3);
        # If there are only 2 array elements, then a year was not provided.
        # Provide the current year so that we match correctly.
        if ($#date == 2)
        {
            $date[4] = $year;
        }
        else
        {
            # Check for a wildcard in a year position
            if (($date[0] eq "*") && ($date[4] eq "*"))
            {
                # Both a wildcard year and month were provided, so let's place
                # the year in the first position forming yyyy/mm/dd
                $date[0] = $year;
            }
            elsif (($date[0] < 13) && ($date[4] eq "*"))
            {
                # A specific month was provided in the first position and
                # a wildcard year was provided in the last position
                $date[4] = $year;
            }
            elsif (($date[4] < 32) && ($date[0] eq "*"))
            {
                # Some digits other than a year were provided in the last
                # position and a wildcard was provided in the first position,
                # so assume the first position is the year.
                $date[0] = $year;
            }
        }

        # Determine the format of the calendar entry
        if ($date[0] > 1900)
        {
            $index = 2;       # We must have yyyy-mm-dd format
        }
        else
        {
            $index = 0;       # We must have mm-dd-yyyy format
        }

        # Support '*' wildcards in month or day positions
        if ($date[$index] eq "*")
        {
            $date[$index] = $mon;
        }
        if ($date[$index+2] eq "*")
        {
            $date[$index+2] = $mday;
        }

        # Check for a match
        if ( (($date[0] == $year) && ($date[2] == $mon) &&
              ($date[4] == $mday)) ||
             (($date[4] == $year) && ($date[0] == $mon) &&
              ($date[2] == $mday)) )
        {
            if ($$print_header eq 1)
            {
                printf("Calendar Entries for %4d-%02d-%02d\n",
                       $year, $mon, $mday);
                print "===============================\n";
                $$print_header = 0;
            }
            print "$date $message";
        }
    }

    close(CAL);
}

#
# MAIN
#
{
    my ($calendar_file,
        @calendars,
        $print_header,
        $timestamp,
        $sec,$min,$hour,$mday,$mon,$year,$wday,$yday);

    # Location of the calendar file
    chdir("$ENV{'HOME'}/calendar");
    $calendar_file = "calendar";
    push(@calendars, $calendar_file);

    # Indicate that the first entry found should result in a header printed
    $print_header = 1;

    if ($#ARGV >= 0)
    {
        $timestamp = int($ARGV[0]);
        ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) =
                                                    localtime($timestamp);
    }
    else
    {
        # Check the calendar for today...
        ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday) = localtime();
    }

    $mon++;
    $year += 1900;

    # Process the calendar array.  Initially, there is only one element,
    # but additional elements might be added by  the "include" lines in the
    # calendar file(s)
    for(my $cal = 0; $cal <= $#calendars; $cal++)
    {
        process_calendar($calendars[$cal],
                         \@calendars,
                         \$print_header,
                         $mon,
                         $mday,
                         $year);
    }

    # If the header was printed, there was a calendar entry printed.
    # Ensure at least one blank line following.
    if ($print_header == 0)
    {
       print "\n";
    }
}
