#!/usr/bin/perl
#
# minder - program to track and remind of calendar events

use strict;
use warnings;

my $caldir = "$ENV{HOME}/.minder"; # where we keep our calendar files
my $quiet = 0; # whether to remind of empty days by just showing the day of week
my @date_args; # list of requested to days on which to search for reminders

# What's today?
my (undef, undef, undef, $mday, $mon, $year, undef, undef, undef) = localtime(time);
$year += 1900;
$mon += 1;
if ($mon < 10) { $mon = "0" . $mon; }
if ($mday < 10) { $mday = "0" . $mday; }
my $today = $year . $mon . $mday;


my $last_date; # the last date inserted into @date_args, if one has been
foreach (@ARGV) {
	if (/^\d{8}$/) {
		push (@date_args, $_);
		$last_date = $_;
	} elsif (/^\+(\d+)$/ and $last_date) {
		# push a date for each between $last_date and $1 days in the future
		my $i = 1; # start after $last_date, because we already pushed that
		while ($i <= $1) {
			push (@date_args, &add_date($last_date, $i));
			$i++;
		}
		$last_date = 0;
	} elsif (/^\+(\d+)$/) {
		# push a date for each between today and $1 days in the future
		my $i = 0; # so that we push today also
		while ($i <= $1) {
			push (@date_args, &add_date($today, $i));
			$i++;
		}
	} elsif (/^-/) {
		if (/q/) { # loud, print what day it is if no other output
			$quiet = 1;
		} elsif (/v/) { # version and exit
			print "minder version 0.1\n";
			exit(0);
		} elsif (/h/){ # help and exit
			&usage(0);
		} else {
			print STDERR "ERROR: bad option\n";
			&usage(1);
		}
	} else {
		print STDERR "ERROR: invalid argument: $_\n";
		&usage(1);
	}
}
unless (@date_args) {
	push (@date_args, $today);
}

foreach (@date_args) {
	my $this_day = $_;
	my @outs = &main($_);

	if (@outs) {
		print "$this_day\n";
	}
	foreach (@outs) {
		print "  $_\n";
	}
}

exit(0);

sub main () {
	my $req_date;
	if ($_[0]) {
		$req_date = $_[0];
	} else {
		print STDERR "ERROR: no date specified\n";
		exit(1);
	}

	unless ($req_date =~ /^(\d{4})(\d{2})(\d{2})$/) {
		print STDERR "ERROR: invalid date specified\n";
		exit(1);
	}

	my $year_req = $1+0;
	my $month_req = $2+0;
	my $day_req = $3+0;
	my $dayow_req = &get_dayow ($year_req, $month_req, $day_req);

	my @reminders;
	# we'll have a perm file for repeated stuff, and a reminders file for one-shot items
	if (open (PERM, "$caldir/perm")) {
		while(<PERM>) {
			unless (/^\s*#/ or /^\s*$/) {
				push (@reminders, $_);
			}
		}
		close PERM;
	} else {
		print STDERR "ERROR: Cannot open permanent events calender\n";
		exit (1);
	}

	if (open (YEAR, "$caldir/$year_req")) {
		while(<YEAR>) {
			unless (/^\s*#/ or /^\s*$/) {
				push (@reminders, "$year_req $_");
			}
		}
		close YEAR;
	} else {
		print STDERR "WARNING: No year file\n";
	}

	my @todays = ();
	foreach (@reminders)
	{
		chomp;

		# $logic is either "&" or "|" indicating ANDing or ORing of day with dayow
		# $dayow is day of week
		# $rest will become $comment, but we name it this way for readability
		my ($year, $month, $day, $logic, $dayow, $origin, $comment, $rest) = "";
		($year, $month, $day, $logic, $rest) = split (' ', $_, 5);

		# After these ifs, we will have pulled out $logic and $dayow
		if ( $logic eq "&" or $logic eq "|" )
		{
			($dayow, $rest) = split (' ', $rest, 2);
		}
		elsif ($logic =~ /^[&|]/)
		{
			$dayow = substr ($logic, 1);
			$logic = substr ($logic, 0, 1);
		}
		else
		{
			$dayow = $logic;
			$logic = "|";
		}

		($origin, $comment) = split (' ', $rest, 2);
		(undef, $origin, undef) = split (/:/, $origin);

		# We now have major fields seperated. Now we have to parse range and discrete fields further

		$month =~ s/jan/1/i;
		$month =~ s/feb/2/i;
		$month =~ s/mar/3/i;
		$month =~ s/apr/4/i;
		$month =~ s/may/5/i;
		$month =~ s/jun/6/i;
		$month =~ s/jul/7/i;
		$month =~ s/aug/8/i;
		$month =~ s/sep/9/i;
		$month =~ s/oct/10/i;
		$month =~ s/nov/11/i;
		$month =~ s/dec/12/i;

		$dayow =~ s/sun/0/i;
		$dayow =~ s/mon/1/i;
		$dayow =~ s/tue/2/i;
		$dayow =~ s/wed/3/i;
		$dayow =~ s/thu/4/i;
		$dayow =~ s/fri/5/i;
		$dayow =~ s/sat/6/i;
		$dayow =~ s/7/0/;

		# hashes to store whether said value matches request
		my %years = &gen_list ($year);
		my %months = &gen_list ($month);
		my %days = &gen_list ($day);
		my %dayows = &gen_list ($dayow);

		my $curr = 1; # bool to see if we'll display the current reminder - true by default

		if (%years)  {# if uninitialized, then all years apply
			unless ($years{$year_req}) {
				$curr = 0;
			}
		}
		if (%months) {
			unless ($months{$month_req}) {
				$curr = 0;
			}
		}

		if ($logic eq "|")
		{
			if (%days) {
				if (%dayows) {
					unless ($days{$day_req} or $dayows{$dayow_req}) {
						$curr = 0;
					}
				}
				else {
					unless ($days{$day_req}) {
						$curr = 0;
					}
				}
			}
			else {
				if (%dayows) {
					unless ($dayows{$dayow_req}) {
						$curr = 0;
					}
				}
			}
		}
		elsif ($logic eq "&")
		{
			if (%days) {
				unless ($days{$day_req}) {
					$curr = 0;
				}
			}
			if (%dayows) {
				unless ($dayows{$dayow_req}) {
					$curr = 0;
				}
			}
		}
		else
		{
			die "ERROR: \$logic not set properly: $logic";
		}

		foreach (keys %dayows) {
			# *** if we have a periodic request ***
			if (/\d+\/(\d+)/) {
				#  determine the divisor (x/<divisor>)
				my $divisor = $1;
				#  determine the period (in the current units)
				#  calc_period = period * divisor (in current units)
				my $calc_period = 7 * $divisor;
				#  keep adding calc_period to origin until we hit requested date (leave curr alone) or we go past (curr = 0)
				my $calc_date = $origin;
				$origin = 0;
				while ($calc_date < $req_date) {
					$calc_date = &add_date($calc_date, $calc_period);
				}
				if ($calc_date == $req_date) {
					$curr = 1;
				}
			}
		}

		# just return the reminders, and let somewhere else sort them out..
		if ($curr) {
			if ($origin) {
				$origin =~ /^(\d{4})\d{4}$/;
				if ($year_req >= $1) {
					push (@todays, ($year_req - $1) . " year(s): $comment");
				}
			} else {
				push (@todays, $comment);
			}
		}
	}

	# insert day of week if we have anything else..
	if (!$quiet or @todays) {
		my @days_of_week = ("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday");
		push (@todays, $days_of_week[$dayow_req]);
	}

	return @todays;
}

# calculate the day of the week based on the 8 digit date
sub get_dayow () {
	my $ypart = ($1 % 100) + int((($1 % 100) / 4));
	my @mpart = (6,2,2,5,0,3,5,1,4,6,2,4);
	my $dpart = $3;
	my $total = $ypart + $mpart[$2-1] + $dpart;
	if (int($1/100) == 19) { $total += 1; }
	if (($1 % 4) == 0) {
		 if (($2 == 1) or ($2 == 2)) { $total -= 1; }
	}

	while ($total > 6) { $total -= 7; }

	return $total;
}

# takes a string of comma seperated, dash-indicated ranges and retuns all the valid values in a hash
# i.e. "2-5,7,8,12-15" returns hash of 2,3,4,5,7,8,12,13,14,15 all set to 1
sub gen_list () {
	my %items = ();
	my $item = $_[0];

	unless ($item eq "*") # leave %items uninitialized if so
	{
		my @holder = split (/,/, $item);
		foreach (@holder)
		{
			unless (/-/) {
				$items{$_} = 1;
			}
			else {
				my ($start, $end) = split (/-/);
				if ($start < $end) {
					while ($start <= $end) {
						$items{$start++} = 1;
					}
				}
				else {
					die "ERROR: item \$start:$start not before \$end:$end";
				}
			}
		}
	}
	return %items;
}

# takes an 8-digit date and a number of days to add, and returns the new date
sub add_date () {
	$_ = $_[0];
	my $added = $_[1];
	/^(\d{4})(\d\d)(\d\d)$/; # splits $_ accordingly.. sweet..

	my $year = $1;
	my $month = $2;
	my $day = $3;

	# Make sure it's all numeric so the hash below works and our comparisons are valid
	$day += $added; #Oh, and add the days..
	$month *= 1;

	# months are crazy go nuts..
	# I couln't find a an easy-enough to use package to do this, so I wrote it myself, no doubt introducing numerous bugs..
	my %months = ( 1 => 31,  2 => 28,  3 => 31,
	               4 => 30,  5 => 31,  6 => 30,
   	            7 => 31,  8 => 31,  9 => 30,
      	        10 => 31, 11 => 30, 12 => 31);

	my $leap = 0;

	while ($day > $months{$month}) {
		if ( $month == 2 && (($year % 4) == 0)) {
			if ( 29 == $day )
			{
				$leap = 1;
				$day = 28; # so we don't get stuck in the loop, we'll set it back to 29 later
			}
			else
			{
				$day -= 29;
				$month++;
			}
		}
		else {
			$day -= $months{$month};
			$month++;
		}

		while ($month > 12)
		{
			$month -= 12;
			$year++;
		}
	}

	if ($leap)
	{
		$day = 29;
		$leap = 0;
	}

	while ($month > 12)
	{
		$month -= 12;
		$year++;
	}

	if ($year > 9999)
	{
		print STDERR "WARNING: Calculated year is huge\n";
		$year = 9999;
	}

	$month *= 1; $day *= 1;

	if ($month < 10) {
		$month = "0" . $month;
	}
	if ($day < 10) {
		$day = "0" . $day;
	}

	return $year . $month . $day;
} # End sub add_date

sub usage () {
	print
"USAGE:
	\$ minder <date> +<days> -<qhv>

	<date>    8 digit date YYYYMMDD
	+<days>   span of days from requested date to display reminders from
	-q        quiet output, don't remind on reminderless days
	-h        print this help output and exit
	-v        print version and exit
";
	exit ($_[0]);
}
