#!/usr/bin/perl
# Dayplanner daemon
# The notification daemon for day planner
# Copyright (C) Eskild Hustvedt 2006
# $Id: dayplanner-daemon 467 2006-07-31 18:15:15Z zero_dogg $
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

# FIXME: 00:00 notifications appear on the *wrong day*

use strict;			# Force strict coding
use warnings;			# Tell perl to print warnings
use IO::Socket;			# We use IO::Socket for communication with dayplanner
use IO::Select;			# We use IO::Select to wait for connections and at the same
				#  time wait for a timeout
use Locale::gettext;		# Allow translation, might not be needed
use Getopt::Long;		# Commandline options
use Cwd;			# We need Cwd::realpath to find the notifier
use File::Basename;		# Ditto
use Data::Dumper;		# We need to be able to dump data
use POSIX qw/setsid/;		# We need setsid();

Getopt::Long::Configure ("bundling", 'prefix_pattern=(--|-)');

# Scalars
my $Version = '0.2';
my $RCSRev = '$Id: dayplanner-daemon 467 2006-07-31 18:15:15Z zero_dogg $';
my $MainEventsFile = "events.dpd";		# The filename to read events from
my $BirthdayFile = "birthdays.dpd";		# The filename to read birthdays from
my $SpecialEventsFile = "events_special.dpd";	# The filename to read special events from
my $DaemonFile = 'daemondata.dpd';		# The filename to save and read daemon data from
my $LogfileName = "daemon.log";			# The filename to log to
my $ConfigFile = "dayplanner.conf";		# The dayplanner config
my $SocketName = "dayplannerd";			# The name of the socket to use
my $NotifierName = "dayplanner-notifier";	# The name of the dayplanner notifier
my $Self = $0;
my (
	$NoFork,	$DayplannerDir,		$DaemonSocket,
	$Server,	$ConnectionSelect,	$ForceFork,
	$Shutdown,	$LastAlarm,
	$LastCalculate,	$NotificationID,	$Logfile
);						# Global scalars

my (
	$DebuggingOutput,	$OutputVerbose,	$OutputVeryVerbose,
) = (0,0,0);

# Hashes
my %CalendarContents;		# Contents of the main calendar
my %BirthdayContents;		# Contents of the birthday calendar
my %SpecialEvents;		# Contents of the speical events calendar
my %NotificationHash;		# The internal hash data
my %Config;			# The configuration hash
my %CurrentNotification;

my %AccessStatus = (
	Connected => 0,
	ClientList => {
	},
);				# Hash containing current connection status

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# HELPER FUNCTIONS
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Print formatted --help output
# Usage: PrintHelp("-shortoption", "--longoption", "description");
sub PrintHelp {
	printf "%-4s %-16s %s\n", "$_[0]", "$_[1]", "$_[2]";
}

# Purpose: Load the config file set in $ConfigFile
# Usage: LoadConfig();
sub LoadConfig {
	%Config = ();
	
	# First, set some fallback defaults
	$Config{Events_NotifyPre} = '45min';
	$Config{Events_DayNotify} = '0';
	
	if(-e "$DayplannerDir/$ConfigFile" and open(my $ConfigFile, "<", "$DayplannerDir/$ConfigFile")) {
		while(<$ConfigFile>) {
			next if m/^\s*#/;
			next unless m/=/;
			chomp;
			my $Option = $_;
			my $Value = $_;
			$Option =~ s/^\s*(.*)\s*=.*/$1/;
			$Value =~ s/^.*=\s*(.*)\s*/$1/;
			unless(defined($Value) and length($Value)) {
				next;
			}
			if ($Option eq 'Events_NotifyPre') {
				unless ($Value =~ /^(\d+(min|hrs?){1}|0){1}$/) {
					$Config{Events_NotifyPre} = '30min';
				} else {
					$Config{Events_NotifyPre} = $Value;
				}
			} elsif ($Option eq 'Events_DayNotify') {
				$Config{Events_DayNotify} = $Value;
			}
		}
		close($ConfigFile);
	} else {
		IntVerbose("Unable to load configuration file: $DayplannerDir/$ConfigFile: $!\n");
	}
	($Config{N_Hours}, $Config{N_Minutes}) = ParseNotifyPre($Config{Events_NotifyPre});
}

# Purpose: Append a "0" to a number if it is only one digit.
# Usage: my $NewNumber = AppendZero(NUMBER);
sub AppendZero {
	if ($_[0] =~ /^\d$/) {
		return("0$_[0]");
	}
	return($_[0]);
}

# Purpose: Set the program status
# Usage: SetStatus(STATUS);
sub SetStatus {
	$0 = "dayplanner-daemon [$_[0]]";
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# CORE CALENDAR FUNCTIONS
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Load the calendar contents
# Usage: LoadCalendar();
sub LoadCalendar {
	if(DataLoadTest("$DayplannerDir/$MainEventsFile")) {
		%CalendarContents = do("$DayplannerDir/$MainEventsFile");
	}
	if(DataLoadTest("$DayplannerDir/$BirthdayFile")) {
		%BirthdayContents = do("$DayplannerDir/$BirthdayFile");
	}
	if(DataLoadTest("$DayplannerDir/$SpecialEventsFile")) {
		%SpecialEvents = do("$DayplannerDir/$SpecialEventsFile");
	}
	# Calculate the notification hash
	CalculateNotificationHash();
	return(1);
}

# Purpose: Test if a load can be successfully performed of the supplied file
# Usage: DataLoadTest("/path/to/file");
sub DataLoadTest {
	# If the calendar savefile exists
	if (-e "$_[0]") {
		# If it isn't readable then we warn and return
		unless (-r "$_[0]") {
			IntWarn("Unable to read the calendar at $_[0]: It isn't readable by me");
			return(0);
		}
		return(1);
	} else {
		# If the file doesn't exist then we just assume that it isn't created yet,
		# so we return false, but do it silently.
		return(0);
	}
}

# Purpose: Find out how many seconds we should sleep before performing some action
# Usage: $SleepTime = FindSleepDuartion();
sub FindSleepDuration {
	# Get the current time/date
	my ($currsec,$currmin,$currhour,$currmday,$currmonth,$curryear,$currwday,$curryday,$currisdst) = localtime(time);
	$curryear += 1900; $currmonth++;

	my @Keys;	# An array containing all the keys in a random order
	
	# Push the keys onto the @Keys array
	foreach(keys(%NotificationHash)) {
		s/^(prenot|tomorrow)_//;
		push(@Keys, $_);
	}
	my ($hours, $minutes);
	# If keys is true then...
	if (@Keys) {
		@Keys = sort(@Keys);
		foreach (@Keys) {
			unless ($_ =~ /^\d+:\d+$/) {
				IntWarn("Invalid event time: $_");
				next;
			}
			my ($testhours, $testminutes) = split(/:/, $_);
			# Remove leading zeros from the variables
			$testhours =~ s/^0(.*)/$1/;
			$testminutes =~ s/^0(.*)/$1/;
			# TODO: This might need to become a bit more robust
			if($testhours < $currhour) {
				next;
			}
			if($testhours == $currhour and $testminutes < $currmin) {
				next;
			}
			if(defined($LastAlarm) and $LastAlarm eq "$testhours:$testminutes") {
				next;
			}
			$hours = $testhours;
			$minutes = $testminutes;
			last;
		}
	}
	# If keys is NOT set then we return the amount of seconds until midnight instead
	unless(defined($hours) and defined($minutes)) {
		$hours = 24;
		$minutes = 0;
	}
	# Convert hours to seconds
	$hours = $hours * 60 * 60;
	# Minutes to seconds
	$minutes = $minutes * 60;
	# Hours to seconds
	$currhour = $currhour * 60 * 60;
	# Minutes to seconds
	$currmin = $currmin * 60;
	# Only do it if $currsec is > 0
#	if($currsec) {
#		# Find the correct currsec value
#		$currsec = 59 - $currsec;
#	}
	# Calculate the total
	my $total = ($hours + $minutes) - ($currhour + ($currmin + $currsec));
	# Return the total to caller
	return($total);
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# CORE HELPER FUNCTIONS
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Output a debugging message if needed
# Usage: IntDebug(MESSAGE);
sub IntDebug {
	if ($DebuggingOutput) {
		IntOutputMSG("DEBUG: $_[0]\n");
	}
}

# Purpose: Output a message when in very verbose mode
# IntVeryVerbose(MESSAGE);
sub IntVeryVerbose {
	if ($OutputVeryVerbose) {
		IntOutputMSG("$_[0]\n");
	}
}

# Purpose: Output a message when in verbose mode
# Usage: IntVerbose(MESSAGE);
sub IntVerbose {
	if ($OutputVerbose) {
		IntOutputMSG("$_[0]\n");
	}
}

# Purpose: Output a properly formatted internal message
# Usage: IntOutputMSG(MESSAGE);
sub IntOutputMSG {
	my ($lsec,$lmin,$lhour,$lmday,$lmon,$lyear,$lwday,$lyday,$lisdst) = localtime(time);
	$lhour = "0$lhour" unless $lhour >= 10;
	$lmin = "0$lmin" unless $lmin >= 10;
	$lsec = "0$lsec" unless $lsec >= 10;
	print "[$lhour:$lmin:$lsec] $_[0]";
}

# Purpose: Print a warning to STDERR with proper output
# Usage: IntWarn("Warning");
sub IntWarn {
	warn "Warning: $_[0]\n";
}

# Purpose: Parse the configuration option set in Events_NotifyPre
# Usage: my ($Hours, $Minutes) = ParseNotifyPre($Events_NotifyPre);
sub ParseNotifyPre {
	my $Events_NotifyPre = $_[0];

	my $Number = $Events_NotifyPre;
	my $Type = $Events_NotifyPre;
	$Number =~ s/^(\d+).*$/$1/;
	unless($Number =~ /\d/) {
		IntWarn("The Events_NotifyPre configuration option is invalid, I was unable to parse it. Pre-event notifications won't work!");
		return(0,0);
	}
	$Type =~ s/^\d+//;
	$Type =~ s/s$//;
	if($Type eq 'hr') {
		return($Number,0);
	} elsif($Type eq 'min') {
		if ($Number > 59) {
			return(0, 59);
		} else {
			return(0,$Number);
		}
	}
	IntWarn("The Events_NotifyPre configuration option is invalid, I was unable to parse it. Pre-event notifications won't work!");
	return(0,0);
}

# Purpose: Return the pre-notification time for an event
# Usage: my $NewTime = GetPreNotTime("OLD:TIME", $PreNotificationHours, $PreNotificationMinutes, IS_TOMORROW);
# 
# OLD:TIME is the time we're working on
# $PreNotificationHours is the hours returned by ParseNotifyPre();
# $PreNotificationMinutes is the minutes returned by ParseNotificationPre();
# IS_TOMORROW is true if the time we got is for tomorrow, not the current day,
# 	we need to process the time more if IS_TOMORROW is true
sub GetPreNotTime {
	# The parsed prenotification hours/minutes
	# We don't need to do any checks on them here, we just assume they are valid
	# as ParseNotifyPre() has already validated them
	my $PreNot_Hours = $_[1];
	my $PreNot_Minutes = $_[2];

	my $Is_Tomorrow = $_[3];
	
	# If they're both zero then we just return undef and don't think about it any more
	if($PreNot_Hours <= 0 and $PreNot_Minutes <= 0) {
		return(undef);
	}
	
	# Get and parse the time
	my $Hours = $_[0];
	my $Minutes = $_[0];
	$Hours =~ s/^(\d+):\d+$/$1/;
	$Minutes =~ s/^\d+:(\d+)$/$1/;
	
	# If $Is_Tomorrow is true we need to do some special processing
	if ($Is_Tomorrow) {
		# If PreNot_Hours is true then we work on hours (easiest:)
		if($PreNot_Hours) {
			my $TestHours = $Hours - $PreNot_Hours;
			if ($TestHours > 0) {
				return(undef);
			} else {
				my $Today_Hours = $PreNot_Hours - $Hours;
				my $New_Hours = 24 - $Today_Hours;
				$New_Hours = AppendZero($New_Hours);
				return("$New_Hours:$Minutes");
			}
		} else {	# If it isn't then we work on minutes, this is harder
			if ($Minutes > $PreNot_Minutes) {
				# Nothing to do
				return(undef);
			} elsif ($Minutes == $PreNot_Minutes) {
				# Nothing to do
				return(undef);
			} else {
				unless($Hours =~ /^00?$/) {
					return(undef);
				}
				$Hours = 23;
				my $New_Minutes = $PreNot_Minutes - $Minutes;
				$Minutes = 60 - $New_Minutes;
				$Minutes = $Minutes + 60;
				$Minutes = AppendZero($Minutes);
				return("$Hours:$Minutes");
			}
		}
	} else {
		# Get the current time/date
		my ($currsec,$currmin,$currhour,$currmday,$currmonth,$curryear,$currwday,$curryday,$currisdst) = localtime(time);
		$curryear += 1900; $currmonth++;

		# Don't do the calculation if the event has already occurred
		if($Hours <= $currhour and $Minutes <= $currmin) {
			return(undef);
		}
		
		# If PreNot_Hours is true then we work on hours (easiest:)
		if($PreNot_Hours) {
			$Hours = $Hours - $PreNot_Hours;
			$Hours = AppendZero($Hours);
			if ($Hours > 0) {
				return("$Hours:$Minutes");
			} else {
				return(undef);
			}
		} else {	# If it isn't then we work on minutes, this is harder
			if ($Minutes > $PreNot_Minutes) {
				# Yay, okay, we can do it the easy way :D
				$Minutes = $Minutes - $PreNot_Minutes;
				$Minutes = AppendZero($Minutes);
				return("$Hours:$Minutes");
			} elsif ($Minutes == $PreNot_Minutes) {
				# Even easier :)
				return("$Hours:00");
			} else {
				if($Hours =~ /^00?$/) {
					return(undef);
				}
				$Hours = $Hours - 1;
				$Minutes = $Minutes + 60;
				$Minutes = $Minutes - $PreNot_Minutes;
				$Minutes = AppendZero($Minutes);
				$Hours = AppendZero($Hours);
				return("$Hours:$Minutes");
			}
		}
	}
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# CORE DAEMON FUNCTIONS
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Purpose: Start the notifier and display the alarm
# Usage: DayplannerAlarm();
sub DayplannerAlarm {
	IntDebug("Alarm triggered");
	
	my $NotifierStarted;
	
	# Get the current time/date
	my ($currsec,$currmin,$currhour,$currmday,$currmonth,$curryear,$currwday,$curryday,$currisdst) = localtime(time);
	$LastAlarm = "$currhour:$currmin";
	# Fix the format
	$currhour = "0$currhour" unless $currhour >= 10;
	$currmin = "0$currmin" unless $currmin >= 10;
	$currsec = "0$currsec" unless $currsec >= 10;
	$curryear += 1900;
	$currmonth++;
	if(defined($NotificationHash{"$currhour:$currmin"})) {
		# TODO: Do we need the check for PreNotification?
		unless(defined($NotificationHash{"$currhour:$currmin"}{PreNotification})) {
			RunNotifier($NotificationHash{"$currhour:$currmin"}{summary},
				$NotificationHash{"$currhour:$currmin"}{fulltext},
				'today',
				"$currhour:$currmin");
			$NotifierStarted = 1;
		} 
	} 
	if(defined($NotificationHash{"prenot_$currhour:$currmin"})) {
		RunNotifier($NotificationHash{"prenot_$currhour:$currmin"}{summary},
			$NotificationHash{"prenot_$currhour:$currmin"}{fulltext},
			'today',
			$NotificationHash{"prenot_$currhour:$currmin"}{PreNotification});
		$NotifierStarted = 1;
	} 
	if(defined($NotificationHash{"tomorrow_$currhour:$currmin"})) {
		RunNotifier($NotificationHash{"tomorrow_$currhour:$currmin"}{summary},
			$NotificationHash{"tomorrow_$currhour:$currmin"}{fulltext},
			'tomorrow',
			"$currhour:$currmin");
		$NotifierStarted = 1;
	}
       unless($NotifierStarted) {
		IntWarn("(DayplannerAlarm) Error: Unable to find an event at $currhour:$currmin") if($LastCalculate eq "$curryear-$currmonth-$curryday-$currmday");
	}
}

# Purpose: Run the notifier itself with the correct parameters
# Usage: RunNotifier(SUMMARY, FULLTEXT, DATE, TIME);
sub RunNotifier {
	# Notifier
	my ($Summary, $Fulltext, $Date, $Time) = @_;
	IntDebug("Alarm! Task: $Summary at $Time on $Date");
	foreach(split(/:/, sprintf("%s:%s", $ENV{PATH}, dirname(Cwd::realpath($Self))))) {
		if ( -x "$_/$NotifierName") {
			$NotificationID++;
			$CurrentNotification{$NotificationID}{Summary} = $Summary;
			$CurrentNotification{$NotificationID}{Fulltext} = $Fulltext;
			$CurrentNotification{$NotificationID}{Time} = $Time;
			$CurrentNotification{$NotificationID}{Date} = $Date;
			# Yes this is a bit weird statement. But remember that commands return 0 on TRUE
			# and 1-255 on FALSE, so it's the other way around from perl
			unless(system("$_/$NotifierName --socket \"$DaemonSocket\" --id \"$NotificationID\"")) {
				return(1);
			}
		}
	}
	IntWarn("Unable to start the notifier! Task: $Summary at $Time on $Date");
}

# Purpose: Handle commands
# Usage: CommandHandler(COMMAND);
sub CommandHandler {
	SetStatus("processing");
	$_ = $_[0];
	my $PID = $_[0];
	$PID =~ s/^(\d+)\s+(.*)/$1/;
	unless ($PID) {
		IntDebug("Malformed request: $_");
		return("ERR MALFORMED_REQUEST");
	}
	study();
	s/^(\d+)\s+//;
	# HI is the first command clients should run to identify themselves
	if (/^HI\s+(\w+)/) {
		my $TYPE = $1;
		IntDebug("New connection ($TYPE), $PID says HI");
		if ($TYPE =~ /client/) {
			# We already have a client, refuse
			if ($AccessStatus{Connected}) {
				if ($_ =~ /FORCE/ ) {
					IntVeryVerbose("$PID is forcing a connection");
				} elsif (defined($AccessStatus{Connected}) and $PID eq $AccessStatus{Connected}) {
					IntDebug("$PID appears to have lost its connection to the daemon and wants to reconnect - allowing it to do so.");
				} else {
					IntVeryVerbose("Refusing connection from $PID");
					delete($AccessStatus{ClientList}{$PID});
					return("REFUSED");
				}
			}
			$AccessStatus{Connected} = $PID;
		} elsif ($TYPE =~ /commander/) {
			IntDebug("New commander ($PID)");
		} elsif ($TYPE =~ /servant/) {
			IntDebug("New servant ($PID)");
		} else {
			IntDebug("Invalid type from $PID ($TYPE) - refusing");
			return("REFUSED");
		}
		$AccessStatus{ClientList}{$PID} = $TYPE;
		return("HI")
	}
	
	# Okay it isn't HI, check if we allow connections from it
	unless($AccessStatus{ClientList}{$PID}) {
		IntDebug("Disallowed client $PID wanted to run command $_ but was refused");
		return("REFUSED");
	}
	
	if (/^BYE/) {	# BYE is the last command clients should run, 
			# removes them from the internal access lists and unlocks if needed
		IntDebug("Closed connection, $PID says BYE");
		if($AccessStatus{ClientList}{$PID} eq 'client') {
			$AccessStatus{Connected} = 0;
		}
		delete($AccessStatus{ClientList}{$PID});
		return("BYE");
	} elsif (/^RELOAD_DATA/) {	# Command to force the daemon to reload the data files
		IntDebug("Data reload requested by $PID");
		LoadCalendar();
		IntDebug("Done reloading data");
		return("done");
	} elsif (/^RELOAD_CONFIG/) {	# Command to force the daemon to reload the config file
		IntDebug("Config reload requested by $PID");
		LoadConfig();
		IntDebug("Done reloading config");
		return("done");
	} elsif (/^SHUTDOWN/) {		# Command to force daemon shutdown
		IntDebug("Shutdown request");
		if($AccessStatus{ClientList}{$PID} =~ /servant/) {
			return("ERR PERMISSION_DENIED");
		} else {
			$Shutdown = "Daemon shut down by client ($PID) request";
			return("okay");
		}
	} elsif (/^PING/) {		# Simple ping implementation
		IntDebug("Ping request from $PID");
		return("PONG");
	} elsif (/^VERSION/) {
		IntDebug("VERSION request from $PID");
		return($Version);
	} elsif (/^GET_PATH/) {
		IntDebug("GET_PATH request from $PID");
		return(Cwd::realpath($Self));
	} elsif (s/^NOTIFICATION\s*//) {
		if (s/^GET\s*//) {
			my $ID = $_;
			my $Variable = $_;
			$ID =~ s/^(.+)\s+.*/$1/;
			$Variable =~ s/^.+\s+(.+)/$1/;
			
			unless(defined($CurrentNotification{$ID})) {
				return("NOT_SET");
			}
			unless(defined($CurrentNotification{$ID}{$Variable})) {
				return("$ID UNDEF");
			}
			return(PackageMultiLine($CurrentNotification{$ID}{$Variable}));
		} elsif (s/^VERIFY\s*//) {
			unless(defined($CurrentNotification{$_})) {
				return("NOT_SET");
			}
			return("SET");
		} elsif (s/^CLEAN\s*//) {
			unless(defined($CurrentNotification{$_})) {
				return("NOT_SET");
			}
			delete($CurrentNotification{$_});
			return("DONE");
		} else {
			return("ERR INVALID_COMMAND");
		}
	}
	elsif (s/^DEBUG\s*//) {
		if (/^DUMP_VARIOUS/) {
			IntDebug("Recieved debugging info request from $PID");
			return("Version $Version - RCSRev $RCSRev || Using dir $DayplannerDir (socket name $SocketName) || AccessStatus{Connected} = $AccessStatus{Connected} || Output: $DebuggingOutput/$OutputVeryVerbose/$OutputVerbose || Outputting to $Logfile");
		} elsif (/^DUMP_CLIENTLIST/) {
			IntDebug("Client list request from $PID");
			my $ClientList;
			foreach (sort keys(%{$AccessStatus{ClientList}})) {
				$ClientList .= $_;
			}
			return($ClientList);
		} elsif (/^DUMP_SLEEPTIME/) {
			IntDebug("Sleep time request from $PID");
			return(FindSleepDuration());
		} elsif (/^KICK\s+(\w+)/) {
			my $CLIENT = $1;
			IntDebug("Client KICK request from $PID");
			if(defined($AccessStatus{ClientList}{$CLIENT})) {
				delete($AccessStatus{ClientList}{$CLIENT});
				return("CLIENT KICKED");
			} else {
				return("UNKNOWN CLIENT");
			}
		} elsif (/^ENABLE_DEBUGLOG/) {
			$DebuggingOutput = 1;
			$OutputVeryVerbose = 1;
			$OutputVerbose = 1;
			IntDebug("Enabled debugging logging as requested by $PID");
			return("SUCCESS");
		} elsif (/^RECALCULATE/) {
			CalculateNotificationHash();
			return("done");
		} elsif (/^(DUMP_HASHES|DUMP_MAINHASH|DUMP_CONFIG)/) {
			# We want the dumper to be pure (aka. make dumper output a proper syntax that is suitable for eval() or do())
			$Data::Dumper::Purity = 1;
			# Sort the keys
			$Data::Dumper::Sortkeys = 1;
			# Set the indentation
			$Data::Dumper::Indent = 1;
			my $Return = '';
			if(/^(DUMP_HASHES|DUMP_MAINHASH)/) {
				$Return = Data::Dumper->Dump([\%NotificationHash], ["*NotificationHash"]);
				if (/^DUMP_HASHES/) {
					$Return = $Return . Data::Dumper->Dump([\%CalendarContents], ["*CalendarContents"]);
					$Return = $Return . Data::Dumper->Dump([\%BirthdayContents], ["*BirthdayContents"]);
					$Return = $Return . Data::Dumper->Dump([\%SpecialEvents], ["*SpecialEvents"]);
					$Return = $Return . Data::Dumper->Dump([\%CurrentNotification], ["*CurrentNotification"]);
				}
			} elsif (/^DUMP_CONFIG/) {
				$Return = $Return . Data::Dumper->Dump([\%Config], ["*Config"]);
			}
			return(PackageMultiLine($Return));
		} else {
			IntDebug("Invalid debug command: \"$_\" from $PID");
			return("ERR INVALID_COMMAND");
		}
	}
	else {				# Handler for invalid requests
		IntDebug("Invalid command: $_ from $PID");
		return("ERR INVALID_COMMAND");
	}
}

# Purpose: Handle SIGPIPE gently
# Usage: $SIG{PIPE} = \&SigpipeHandler;
sub SigpipeHandler {
	IntDebug("Sigpipe");
}

# Purpose: Open our main communication socket
# Usage: OpenSocket();
sub OpenSocket {
	if (-e $DaemonSocket) {
		my $TestSocket = IO::Socket::UNIX->new(Peer	=> $DaemonSocket,
							Type	=> SOCK_STREAM
							Timeout => 2);
		if (defined($TestSocket)) {
			# We could connect
			#
			# Now this isn't a nice way to do it, but we do it anyway
			print $TestSocket "$$ HI commander\n";
			my $CommanderReply = <$TestSocket>;
			print $TestSocket "$$ PING\n";
			my $REPLY = <$TestSocket>;
			chomp($REPLY);
			if ($REPLY eq "PONG") {
				# It's still responding. If the user did this then perhaps he/she wanted
				# to reload the data. Send the reload command
				print $TestSocket "$$ RELOAD_DATA\n";
				print $TestSocket "$$ BYE\n";
				die "Error: A dayplanner daemon is already running and still responding. I told it to reload its data files.\n";
			}
		}
		unlink($DaemonSocket);
				
	}
	$Server = IO::Socket::UNIX->new(
					Local	=> $DaemonSocket,
					Type	=> SOCK_STREAM,
					Listen	=> 5,
			) or die "Unable to create a new socket: $@\n";
	# Trap SIGPIPE
	$SIG{PIPE} = \&SigpipeHandler;
	# Create a new select handle for reading
	$ConnectionSelect = IO::Select->new();
	# Add the main server
	$ConnectionSelect->add($Server);
}

# Purpose: Start the main loop
# Usage: MainLoop();
# Requires: OpenSocket(); already performed
sub MainLoop {
	# Loop for eternity
	while (1) {
		my $SleepTime = FindSleepDuration();
		SetStatus("sleeping");
		# Block until one handle is available or it times out
		my @Ready_Handles = $ConnectionSelect->can_read($SleepTime);
		# Timeout is true if no handle was processed
		my $Timeout = 1;
		# For each handle...
		foreach my $Handle (@Ready_Handles) {
			# We didn't timeout
			$Timeout = 0;
			# If the handle is $server then it's a new connection
			if ($Handle eq $Server) {
				IntDebug("New connection");
				my $NewClient = $Server->accept();
				$ConnectionSelect->add($NewClient);
			} 
			# Handle isn't $server, it's an existing connection trying to tell us something
			else {
				# What is it trying to tell us?
				my $Command = <$Handle>;
				# If it is defined then it's a command
				if ($Command) {
					chomp($Command);
					my ($Reply) = CommandHandler($Command);
					print $Handle "$Reply\n";
				} 
				# If it isn't, then it closed the connection
				else {
					IntDebug("Connection closed");
					$ConnectionSelect->remove($Handle);
				}
			}
		}
		if ($Timeout) {
			DayplannerAlarm();
		}
		if ($Shutdown) {
			IntVerbose($Shutdown);
			unlink($DaemonSocket);
			exit(0);
		}
		# Get the current time/date
		my ($currsec,$currmin,$currhour,$currmday,$currmonth,$curryear,$currwday,$curryday,$currisdst) = localtime(time);
		$curryear += 1900; $currmonth++;
		# Recalculate our notification hash if needed
		unless($LastCalculate eq "$curryear-$currmonth-$curryday-$currmday") {
			CalculateNotificationHash();
		}
	}
}

# Purpose: Calculate todays %NotificationHash
# Usage: CalculateNotificationHash();
sub CalculateNotificationHash {
	%NotificationHash = ();
	
	# Get the current time/date
	my ($currsec,$currmin,$currhour,$currmday,$currmonth,$curryear,$currwday,$curryday,$currisdst) = localtime(time);
	$curryear += 1900; $currmonth++;
	
	# Set LastCalculate to the current time/date
	$LastCalculate = "$curryear-$currmonth-$curryday-$currmday";
	
	# First, calculate todays part of the %CalendarContents hash
	if (defined($CalendarContents{$curryear}{$currmonth}{$currmday})) {
		foreach my $Time (keys(%{$CalendarContents{$curryear}{$currmonth}{$currmday}})) {
			$NotificationHash{$Time}{summary} = $CalendarContents{$curryear}{$currmonth}{$currmday}{$Time}{summary};
			if(defined($CalendarContents{$curryear}{$currmonth}{$currmday}{$Time}{fulltext})) {
				$NotificationHash{$Time}{fulltext} = $CalendarContents{$curryear}{$currmonth}{$currmday}{$Time}{fulltext};
			}
			my $PreNot_Time = GetPreNotTime($Time, $Config{N_Hours}, $Config{N_Minutes}, 0);
			if(defined($PreNot_Time)) {
				$PreNot_Time = "prenot_$PreNot_Time";
				# TODO: We need to make sure another event isn't ocurring at the same time (append a digit or something to it?)
				$NotificationHash{$PreNot_Time}{summary} = $CalendarContents{$curryear}{$currmonth}{$currmday}{$Time}{summary};
				if(defined($CalendarContents{$curryear}{$currmonth}{$currmday}{$Time}{fulltext})) {
					$NotificationHash{$PreNot_Time}{fulltext} = $CalendarContents{$curryear}{$currmonth}{$currmday}{$Time}{fulltext};
				}
				$NotificationHash{$PreNot_Time}{PreNotification} = $Time;
			}
				
		}
	}
	# Now, calculate tomorrows part of the %CalendarContents hash
	#  this so that in case there's an event tomorrow that has a PerNotTime that would be
	#  *today* it does pop up. This will probably be rare, but we need to handle it
	#  none-the-less.
	my ($tomorrowsec,$tomorrowmin,$tomorrowhour,$tomorrowmday,$tomorrowmonth,$tomorrowyear,$tomorrowwday,$tomorrowyday,$tomorrowisdst) = localtime(time+86_400);
	$tomorrowyear += 1900; $tomorrowmonth++;
	if (defined($CalendarContents{$tomorrowyear}{$tomorrowmonth}{$tomorrowmday})) {
		foreach my $Time (keys(%{$CalendarContents{$tomorrowyear}{$tomorrowmonth}{$tomorrowmday}})) {
			my $PreNot_Time = GetPreNotTime($Time, $Config{N_Hours}, $Config{N_Minutes}, 1);
				if(defined($PreNot_Time)) {
					$PreNot_Time = "prenot_$PreNot_Time";
					# TODO: We need to make sure another event isn't ocurring at the same time (append a digit or something to it?)
					$NotificationHash{$PreNot_Time}{summary} = $CalendarContents{$tomorrowyear}{$tomorrowmonth}{$tomorrowmday}{$Time}{summary};
					if(defined($CalendarContents{$tomorrowyear}{$tomorrowmonth}{$tomorrowmday}{$Time}{fulltext})) {
						$NotificationHash{$PreNot_Time}{fulltext} = $CalendarContents{$tomorrowyear}{$tomorrowmonth}{$tomorrowmday}{$Time}{fulltext};
					}
					$NotificationHash{$PreNot_Time}{PreNotification} = $Time;
				}
		}
	}

	# Now append tomorrows events if Events_DayNotify is true
	if($Config{Events_DayNotify} eq '1') {
		if (defined($CalendarContents{$tomorrowyear}{$tomorrowmonth}{$tomorrowmday})) {
			foreach my $Time (keys(%{$CalendarContents{$tomorrowyear}{$tomorrowmonth}{$tomorrowmday}})) {
				my $SetTime = "tomorrow_$Time";
				$NotificationHash{$SetTime}{summary} = $CalendarContents{$tomorrowyear}{$tomorrowmonth}{$tomorrowmday}{$Time}{summary};
				if(defined($CalendarContents{$tomorrowyear}{$tomorrowmonth}{$tomorrowmday}{$Time}{fulltext})) {
					$NotificationHash{$SetTime}{fulltext} = $CalendarContents{$tomorrowyear}{$tomorrowmonth}{$tomorrowmday}{$Time}{fulltext};
				}
			}
		}
	}

}

# Purpose: Wrap multiline packets correctly
# Usage: PackageMultiLine(SCALAR);
sub PackageMultiLine {
	my $Message = $_[0];
	my $NewMessage = "BEGIN MULTILINE\n";
	foreach(split(/\n/,$Message)) {
		chomp;
		$NewMessage = $NewMessage . "ML $_\n";
	}
	unless($Message =~ /\n$/) {
		# The preceeding line doesn't end with \n so set a "last line didn't end with \n" option
		$NewMessage = $NewMessage . "NNL\n";
	}
	$NewMessage = $NewMessage . "\nEND MULTILINE";

	return($NewMessage);
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# INITIALIZATION
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

GetOptions (
	'help|h' => sub {
		print "Day planner daemon version $Version\n\n";
		PrintHelp("-d", "--dayplannerdir", "Which directory to use as the dayplanner config dir");
		PrintHelp("", "--version", "Display version information and exit");
		PrintHelp("-h,", "--help", "Display this help screen");
		PrintHelp("-n,", "--nofork", "Don't go into the background (and log to STDOUT/STDERR instead of the logfile)");
		PrintHelp("-v,", "--verbose", "Be verbose");
		PrintHelp("-V,", "--veryverbose", "Be very verbose");
		PrintHelp("-D,", "--debug", "Output debugging information");
		PrintHelp("-o,", "--output", "Output all messages to the file supplied instead of dayplannerdir/$LogfileName");
		exit(0);
	},
	'debug|D' => sub {
		$DebuggingOutput = 1;
		$OutputVerbose = 1;
		$OutputVeryVerbose = 1;
	},
	'verbose|v' => \$OutputVerbose,
	'veryverbose|V' => sub {
		$OutputVerbose = 1;
		$OutputVeryVerbose = 1;
	},
	'n|nofork' => \$NoFork,
	'f|force-fork' => \$ForceFork,
	'o|output=s' => \$Logfile,
	'dayplannerdir|d=s' => sub {
		unless (-e $_[1]) {
			die "$_[1] does not exist\n";
		}
		unless (-w $_[1]) {
			die "I can't write to $_[1]\n";
		}
		$DayplannerDir = $_[1];
	},
	'socketname|s=s' => sub {
		if ($_[1] =~ m#/#) {
			die "The --socketname can't contain a /\n";
		}
		$SocketName = $_[1];
	},
	'version' => sub {
		print "Day planner daemon version $Version\n";
		print "RCS revision: $RCSRev\n";
		exit(0);
	},
) or die "See $0 --help for more information\n";

# Set some settings according to commandline arguments
$DayplannerDir = "$ENV{HOME}/.dayplanner" unless defined($DayplannerDir);
$DaemonSocket = "$DayplannerDir/$SocketName";

# Daemonize unless NoFork is true
if (not $NoFork or $ForceFork) {
	# Fork
	my $PID = fork;
	exit if $PID;
	die "Unable to fork: $!\nYou may want to try --nofork\n" unless defined($PID);
	# Create a new session
	setsid() or IntWarn "Unable to start a new POSIX session (setsid()): $!";
	# Change dir to / - this to avoid clogging up a mountpoint
	chdir("/") or IntWarn "Unable to chdir to /: $!";
	# (We finish the daemonizing after opening the socket, loading the config and calendar)
}

# Verify the dir
die "$DayplannerDir doesn't exist!\n" unless -e $DayplannerDir;
die "$DayplannerDir is not a directory\n" unless -d $DayplannerDir;

$Logfile = "$DayplannerDir/$LogfileName" unless defined($Logfile) and length($Logfile);;

# Main
SetStatus("starting");
OpenSocket();
LoadConfig();
LoadCalendar();

unless($NoFork) {
	open(STDIN, "<", "/dev/null") or IntWarn("Couldn't reopen STDIN to /dev/null: $!");
	open(STDOUT, ">>", $Logfile) or IntWarn("Couldn't reopen STDOUT to $Logfile: $!");
	open(STDERR, ">>", $Logfile) or IntWarn("Couldn't reopen STDERR to $Logfile: $!");
}

MainLoop();
