#!/usr/bin/perl
# dayplanner-daemon
# Copyright (C) Eskild Hustvedt 2007
#
# 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 3 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, see <http://www.gnu.org/licenses/>.

use strict;
use warnings;

# We use IO::Socket for communication with dayplanenr and
# IO::Select to wait for connections and a timeout
use IO::Socket;
use IO::Select;
# These constants prettify things
use constant { true => 1, false => 0 };
# Parameter parser
use Getopt::Long;
Getopt::Long::Configure ('bundling', 'prefix_pattern=(--|-)');
# Used to locate the notifier
use Cwd;
use File::Basename;
# We need mktime and setsid
use POSIX qw/ mktime setsid/;		# We need setsid();
# Used to locate our own modules
use FindBin;			# So that we can detect module dirs during runtime
# This here is done so that we can use local versions of our libs
use lib "$FindBin::RealBin/modules/";
use lib "$FindBin::RealBin/modules/DP-iCalendar/lib/";
use lib "$FindBin::RealBin/modules/DP-GeneralHelpers/lib/";
# Day Planner-specific libs
use DP::iCalendar qw(iCal_ConvertToUnixTime); # iCalendar support
use DP::GeneralHelpers qw(DPIntWarn DPIntInfo WriteConfigFile LoadConfigFile AppendZero);

# Hash containing unixtime -> array of events pairs.
# This should contain all the events for the current day
# and all the events for the upcoming day
my %Notifications;
# Hash containing information used for pre-notifications
my %PreNotValues = (
	PreNotSeconds => undef,
	PreNotDay => 1,
);
# Hash containing state information
my %DaemonState = (
	HasNotified => '',
	LastWritten => '',
);

# The path to us
my $Self = $0;
# Set the name
$0 = 'dayplanner-daemon';
# The name of the notifier
my $NotifierName = 'dayplanner-notifier';
# Our version number
my $Version = 0.8.1;
# Our RCS revision
my $RCSRev = '$Id: dayplanner-daemon 1899 2008-01-08 08:40:31Z zero_dogg $';
# The iCalendar object
my $iCalendar;
# The Day Planner directory
my $DataDir; 
# The calendar filename
my $CalendarFile = 'calendar.ics';
# The socket filename
my $OrigSocketName = 'Daemon_Socket';
my $SocketName = $OrigSocketName;
# The socket path
my $SocketPath;
# The socket FH
my $ServerSocket;
# The connection selection object
my $ConnectionSelector;
# Scalar that identifies what the next event is.
# It can be the string DAYCHANGE, if it isn't then it is assumed to be
# an int containing the time of which the next event will occur on.
my $NextEventIs;
# Var that is true if we are to fork no matter what else we might be told
my $ForceFork;
# Var that is true if we are not to fork nor log anything (all to STDOUT).
# ForceFork overrides it.
my $NoFork;

# The current log level
my $LogLevel = 2;
# The log file
my $Logfile;

# Signal handlers
$SIG{INT} = \&Shutdown;
$SIG{TERM} = \&Shutdown;

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

# Purpose: Shut down a currently running daemon
# Usage: ShutdownRunning();
sub ShutdownRunning
{
	my $return = false;
	if (-e $SocketPath) {
		my $TestSocket = IO::Socket::UNIX->new(Peer	=> $SocketPath,
			Type	=> SOCK_STREAM
			Timeout => 2);
		if (defined($TestSocket)) {
			# We could connect
			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 "$$ SHUTDOWN\n";
				$return = true;
			}
			close($TestSocket);
		}
		unlink($SocketPath);
	}
}

# Purpose: Output log information.
# Usage: DaemonLog(LEVEL,Message);
# 	LEVEL is an int:
# 	0  - Critical
# 	1  - Error
# 	2  - Warning
#   3  - Notification
# 	4  - Debugging
#   5  - Calltrace
sub DaemonLog
{
	my $Level = shift;
	return if not($Level <= $LogLevel);
	my $Message = shift;
	my $MsgPrefix;
	if($Level == 0 or $Level == 1)
	{
		$MsgPrefix = 'Error:        ';
	}
	elsif($Level == 2)
	{
		$MsgPrefix = 'Warning:      ';
	}
	elsif($Level == 3)
	{
		$MsgPrefix = 'Notification: ';
	}
	elsif($Level == 4)
	{
		$MsgPrefix = 'Debug:        ';
	}
	elsif($Level == 5)
	{
		$MsgPrefix = 'Calltrace:    ';
	}
	my ($lsec,$lmin,$lhour,$lmday,$lmon,$lyear,$lwday,$lyday,$lisdst) = GetDate(time());
	$lhour = "0$lhour" unless $lhour >= 10;
	$lmin = "0$lmin" unless $lmin >= 10;
	$lsec = "0$lsec" unless $lsec >= 10;
	print "[$lmday/$lmon/$lyear $lhour:$lmin:$lsec] ";
	print($MsgPrefix.$Message);
	print "\n";
}

# Purpose: Parse the configuration option set in Events_NotifyPre
# Usage: $seconds = ParseNotifyPre($Events_NotifyPre);
sub ParseNotifyPre {
	my $Events_NotifyPre = shift;

	my $Number = $Events_NotifyPre;
	my $Type = $Events_NotifyPre;
	my $Total = 0;
	$Number =~ s/^(\d+).*$/$1/;
	if ($Number =~ /\D/)
	{
		DaemonLog(0,'The Events_NotifyPre configuration option is invalid, I was unable to parse it. Pre-event notifications won\'t work!');
		DaemonLog(4,'Unable to parse number: '.$Number);
		return($Total);
	}
	$Type =~ s/^\d+//;
	$Type =~ s/s$//;
	if($Type eq 'hr')
	{
		$Total = $Number * 60 * 60;
	} 
	elsif($Type eq 'min')
	{
		if ($Number < 60)
		{
			$Total = $Number * 60;
		}
		else
		{
			DaemonLog(0,'The Events_NotifyPre configuration option is invalid, I was unable to parse it. Pre-event notifications won\'t work!');
			DaemonLog(4,'Unable to parse number: '.$Number);
		}
	}
	DaemonLog(4,'ParseNotifyPre: '.$Total);
	return($Total);
}

# Purpose: The same as localtime(TIME?); but returns proper years and months
# Usage: my ($currsec,$currmin,$currhour,$currmday,$currmonth,$curryear,$currwday,$curryday,$currisdst) = GetDate(TIME);
#  TIME is optional. If not present then the builtin time function is called.
#  FIXME: MOVE TO GENERALHELPERS
sub GetDate {
	my $Time = $_[0] ? $_[0] : time;
	my ($currsec,$currmin,$currhour,$currmday,$currmonth,$curryear,$currwday,$curryday,$currisdst) = localtime($Time);
	$curryear += 1900;						# Fix the year format
	$currmonth++;							# Fix the month format
	return($currsec,$currmin,$currhour,$currmday,$currmonth,$curryear,$currwday,$curryday,$currisdst);
}

# Purpose: Check if two unixtimes are on the same date
# Usage: OnSameDate(TIME,TIME);
# 	Returns bool
sub OnSameDate
{
	my $Time1 = shift;
	my $Time2 = shift;
	my ($first_sec,$first_min,$first_hour,$first_mday,$first_mon,$first_year,$first_wday,$first_yday,$first_isdst) = GetDate($Time1);
	my ($second_sec,$second_min,$second_hour,$second_mday,$second_mon,$second_year,$second_wday,$second_yday,$second_isdst) = GetDate($Time2);
	if ($first_year eq $second_year and $first_yday eq $second_yday)
	{
		return(true);
	}
	else
	{
		return(false);
	}
}

# Purpose: Detect the user config  directory
# Usage: DetectConfDir();
#  FIXME: MOVE TO GENERALHELPERS
sub DetectConfDir {
	# First detect the HOME directory, and set $ENV{HOME} if successfull,
	# if not we just fall back to the value of $ENV{HOME}.
	my $HOME = getpwuid($>);
	if(-d $HOME) {
		$ENV{HOME} = $HOME;
	}
	# Compatibility mode, using the old conf dir
	if(-d "$ENV{HOME}/.dayplanner") {
		return("$ENV{HOME}/.dayplanner");
	}
	# Check for XDG_CONFIG_HOME in the env
	my $XDG_CONFIG_HOME;
	if(defined($ENV{XDG_CONFIG_HOME})) {
		$XDG_CONFIG_HOME = $ENV{XDG_CONFIG_HOME};
	} else {
		if(defined($ENV{HOME}) and length($ENV{HOME})) {
			# Verify that HOME is set properly
			if(not -d $ENV{HOME}) {
				die("$ENV{HOME}: does not exist\n");
			}
			$XDG_CONFIG_HOME = "$ENV{HOME}/.config";
		} else {
			die("The environment variable HOME is missing\n");
		}
	}
	return("$XDG_CONFIG_HOME/dayplanner");
}

# Purpose: Write the daemon state
# Usage: WriteDaemonState();
sub WriteDaemonState
{
	DaemonLog(5,'Writing daemon state to '.$DataDir . '/daemon_state.conf');
	my %Explenations = (
		HEADER => "This file contains internal configuration used by the Day Planner Daemon (reminder)\n# You really don't want to edit this file manually",
		HasNotified => 'This is a list of UIDs that has already been notified',
		LastWritten => 'The unix time of when this file was last written ('.scalar(localtime()).')',
	);
	if(not OnSameDate(time()),$DaemonState{LastWritten})
	{
		DaemonLog(5,'HasNotified= was not from today. Replacing with an empty string');
		$DaemonState{HasNotified} = '';
	}
	$DaemonState{LastWritten} = time();
	WriteConfigFile($DataDir . '/daemon_state.conf',\%DaemonState, \%Explenations);
}

# Purpose: Load the state file
# Usage: LoadDaemonState();
sub LoadDaemonState
{
	if(-e $DataDir . '/daemon_state.conf')
	{
		DaemonLog(5,'Loading daemon state from '.$DataDir . '/daemon_state.conf');
		LoadConfigFile($DataDir . '/daemon_state.conf', \%DaemonState, undef, 0);
		WriteDaemonState();
	} 
	else
	{
		WriteDaemonState();
	}
}

# Purpose: Load the pre-notification values
# Usage: LoadDPConfig();
sub LoadDPConfig
{
	my %Config;
	LoadConfigFile($DataDir . '/dayplanner.conf',\%Config,undef,false);
	$PreNotValues{PreNotSeconds} = ParseNotifyPre($Config{Events_NotifyPre});
	$PreNotValues{PreNotDay} = $Config{Events_DayNotify};
	DaemonLog(4,"PreNotDay = $PreNotValues{PreNotDay} and PreNotSeconds = $PreNotValues{PreNotSeconds}");
}

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

# Purpose: system() replacement that logs the command run
# Usage: loggingSystem(SAME_AS_system());
sub loggingSystem
{
	DaemonLog(5,join(' ',@_));
	return(system(@_));
}

# ----
# CALENDAR/CALCULATION FUNCTIONS
# ----

# Purpose: Convert HH:MM to seconds, for use with unixtime
# Usage: HumanTimeToSeconds(STRING);
#  Returns a value of the seconds since midnight - just do time()+this value
sub HumanTimeToSeconds
{
	my $time = shift;
	my $hour = $time;
	my $minute = $time;
	# The value can be returned as zero if value is 00:00
	my $timeInSeconds = 0;
	$hour =~ s/^(\d+):.*$/$1/;
	$minute =~ s/^\d+:(\d+)$/$1/;
	if(not $hour == 0)
	{
		$timeInSeconds = $timeInSeconds + ($hour*60*60);
	}
	if(not $minute == 0)
	{
		$timeInSeconds = $timeInSeconds + ($minute*60);
	}
	return($timeInSeconds);
}

# Purpose: Load or reload the calendar data
# Usage: LoadData();
sub LoadData
{
	if($iCalendar)
	{
		DaemonLog(5,'Data reloaded');
		$iCalendar->reload();
		CalculateNotifications();
	}
	else
	{
		$iCalendar = new DP::iCalendar($DataDir.'/'.$CalendarFile);
		DaemonLog(5,'Data loaded from ' .$DataDir.'/'.$CalendarFile);
	}
}

# Purpose: Calculate or recalculate the list of notification events
# Usage: CalculateNotifications();
sub CalculateNotifications
{
	DaemonLog(5,'Calculating notifications');
	# Find today
	my ($sec,$min,$hour,$Day,$Month,$Year,$wday,$yday,$isdst) = localtime(time());
	my $TodayNix = mktime(0,0,0,$Day,$Month,$Year,0,0,$isdst);
	($sec,$min,$hour,$Day,$Month,$Year,$wday,$yday,$isdst) = GetDate(time());
	# Empty the hash
	%Notifications = ();
	# Find events for today. Add to the hash.
	# Main calendar contents
	if (my $TimeArray = $iCalendar->get_dateinfo($Year,$Month,$Day)) {
		foreach my $Time (sort @{$TimeArray}) {
			# If time is DAY then skip it - it's a fullday event
			next if $Time eq 'DAY';
			# Convert time to seconds
			my $SecondsFromMidnight = HumanTimeToSeconds($Time);
			# Get the real unix time
			my $RealTime = $TodayNix + $SecondsFromMidnight;
			# Main notification
			if(not $Notifications{$RealTime})
			{
				$Notifications{$RealTime} = $iCalendar->get_timeinfo($Year,$Month,$Day,$Time);
			}
			else
			{
				push(@{$Notifications{$RealTime}},@{$iCalendar->get_timeinfo($Year,$Month,$Day,$Time)});
			}
			# Pre-notification
			if($PreNotValues{PreNotSeconds})
			{
				my $PreNotTime = $RealTime - $PreNotValues{PreNotSeconds};
				# Main notification
				if(not $Notifications{$PreNotTime})
				{
					$Notifications{$PreNotTime} = $iCalendar->get_timeinfo($Year,$Month,$Day,$Time);
				}
				else
				{
					push(@{$Notifications{$RealTime}},@{$iCalendar->get_timeinfo($Year,$Month,$Day,$Time)});
				}
			}
		}
	}
	# Return if prenotday isn't active
	return if not $PreNotValues{PreNotDay};
	# Find tomorrow
	($sec,$min,$hour,$Day,$Month,$Year,$wday,$yday,$isdst) = GetDate(time() + 86_400);
	# Find events for tom
	if (my $TimeArray = $iCalendar->get_dateinfo($Year,$Month,$Day)) {
		foreach my $Time (sort @{$TimeArray}) {
			# If time is DAY then skip it - it's a fullday event
			next if $Time eq 'DAY';
			# Convert time to seconds
			my $SecondsFromMidnight = HumanTimeToSeconds($Time);
			# Get the real unix time
			# (this is the unix time for that time of day *today*. Ie. the time to run the prenot)
			my $RealTime = $TodayNix + $SecondsFromMidnight;
			# Main notification
			if(not $Notifications{$RealTime})
			{
				$Notifications{$RealTime} = $iCalendar->get_timeinfo($Year,$Month,$Day,$Time);
			}
			else
			{
				push(@{$Notifications{$RealTime}},@{$iCalendar->get_timeinfo($Year,$Month,$Day,$Time)});
			}
		}
	}
}

# ----
# DAEMON FUNCTIONS
# ----

# Purpose: Shut down the daemon
# Usage: Shutdown();
sub Shutdown
{
	DaemonLog(4,'Shutting down');
	# Close filehandles and clean up
	close($ServerSocket);
	# TODO: Do some connection magic here to ensure that we actually were the one listening on it
	unlink($SocketPath);
	# Write the state one last time to ensure it is up to date
	WriteDaemonState();
	# Okay, we're all done
	exit(0);
}

# Purpose: Found out how many seconds we should sleep before we need to perform an action
# Usage: $seconds = FindSleepDuration();
sub FindSleepDuration
{
	# Find tomorrow
	my ($sec,$min,$hour,$Day,$Month,$Year,$wday,$yday,$isdst) = GetDate(time() + 86_400);
	my $TomorrowNix = mktime(0,0,0,$Day,$Month -1,$Year - 1900,0,0,$isdst);
	foreach my $time (sort keys(%Notifications))
	{
		# If it's tomorrow then break out of the loop and sleep until midnight
		if($time >= $TomorrowNix)
		{
			DaemonLog(5,'Sleeptime - is tomorrow, going out of loop: '.$time);
			last;
		}
		# If $time is more than current time then return that
		if($time > time())
		{
			DaemonLog(5,'Found sleeptime - until: '.$time);
			$NextEventIs = $time;
			return($time - time());
		}
	}
	DaemonLog(5,'Sleeping until day changes');
	$NextEventIs = 'DAYCHANGE';
	# We add 1 second here to take into account some irregularities
	return($TomorrowNix-time() + 1);
}

# Purpose: Open our main communication socket
# Usage: OpenSocket();
sub OpenSocket {
	if (-e $SocketPath) {
		my $TestSocket = IO::Socket::UNIX->new(Peer	=> $SocketPath,
							Type	=> SOCK_STREAM
							Timeout => 2);
		if (defined($TestSocket)) {
			# We could connect
			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";
				if ($LogLevel > 2) {
					print $TestSocket "$$ SET_LOGLEVEL $LogLevel\n";
				}
				close($TestSocket);
				if ($LogLevel > 2)
				{
					die "Error: A dayplanner daemon is already running and still responding. I told it to reload its data files and set its LogLevel (verbosity) to $LogLevel.\n";
				}
				else
				{
					die "Error: A dayplanner daemon is already running and still responding. I told it to reload its data files.\n";
				}
			}
			close($TestSocket);
		}
		unlink($SocketPath);
				
	}
	$ServerSocket = IO::Socket::UNIX->new(
					Local	=> $SocketPath,
					Type	=> SOCK_STREAM,
					Listen	=> 5,
			) or die "Unable to create a new socket: $@\n";
	# Enforce strict permissions
	chmod(oct(600),$SocketPath);
	# Trap SIGPIPE
	$SIG{PIPE} = \&SigPipeHandler;
	# Create a new select handle for reading
	$ConnectionSelector = IO::Select->new();
	# Add the main server
	$ConnectionSelector->add($ServerSocket);

	DaemonLog(3,'Now listening on '.$SocketPath);
}

# Purpose: Handle incoming daemon commands
# Usage: $Return = CommandHandler(LINE);
sub CommandHandler 
{
	$_ = shift;
	DaemonLog(5,$_);
	my $PID = $_;
	$PID =~ s/^(\d+)\s+(.*)/$1/;
	unless ($PID) {
		DaemonLog(3,"Malformed request: $_");
		return('ERR MALFORMED_REQUEST');
	}
	study();
	s/^(\d+)\s+//;
	if(/^RELOAD_DATA/)
	{
		LoadData();
		return('done');
	}
	elsif(/^RELOAD_CONFIG/)
	{
		LoadDPConfig();
		return('done');
	}
	elsif(/^SHUTDOWN/)
	{
		Shutdown();
	}
	elsif (/^VERSION/)
	{
		return($Version);
	}
	elsif(/^PING/)
	{
		return('PONG');
	}
	elsif(s/^SET_LOGLEVEL\s+(\d+)/$1/)
	{
		s/\s//g;
		if (/\D/) {
			return('ERR # SYNTAX ERROR');
		}
		$LogLevel = $_;
		DaemonLog(4,"$$ told me to set LogLevel to $_, so I did");
		return('done');
	}
	# Old API
	elsif(/^(HI|BYE|NOTIFICATION|DEBUG|GET_PATH)/)
	{
		DaemonLog(3,'Legacy command attempted run: '.$_);
		return('ERR LEGACY # Something went wrong, program attempted to use legacy command ('.$_.'). Please verify your Day Planner installation. Version 0.8 or later required.');
	}
	else
	{
		return('ERR INVALID_COMMAND');
	}
	
}

# Purpose: Run event notifications for the supplied time
# Usage: EventNotification(UNIXTIME);
sub EventNotification
{
	my $time = shift;
	DaemonLog(4,'EventNotification processing');
	foreach my $UID(@{$Notifications{$time}})
	{
		StartNotifier($UID);
	}
	WriteDaemonState();
}

# Purpose: Start the notifier
# Usage: StartNotifier(UID);
sub StartNotifier
{
	my $UID = shift;
	DaemonLog(4,'Starting notifier for UID '.$UID);
	foreach(split(/:/, sprintf('%s:%s', $ENV{PATH}, dirname(Cwd::realpath($Self)))))
	{
		if ( -x $_.'/'.$NotifierName)
		{
			if(loggingSystem($_.'/'.$NotifierName, '--calendar', $DataDir .'/'.$CalendarFile, '--uid', $UID)  == 0)
			{
				$DaemonState{HasNotified} .= ' '.$UID;
				return(true);
			}
			else
			{
				DaemonLog(2,'Tried running notifier at '.$_.'/'.$NotifierName.' but it exited with a nonzero return value. Continuing search');
			}
		}
	}
}

# Purpose: Go into daemon mode
# Usage: Daemonize();
sub Daemonize
{
	# Fork
	my $PID = fork;
	exit if $PID;
	die "Unable to fork: $!\nYou may want to try --nofork\n" if not defined($PID);
	# Create a new session
	setsid() or DaemonLog(1,"Unable to start a new POSIX session (setsid()): $!");
	# Change dir to / - this to avoid clogging up a mountpoint
	chdir('/') or DaemonLog(1,"Unable to chdir to /: $!");
	# (We finish the daemonizing after loading the config and calendar)
	open(STDIN, '<', '/dev/null') or DaemonLog(1,"Couldn't reopen STDIN to /dev/null: $!");
	open(STDOUT, '>>', $Logfile) or DaemonLog(1,"Couldn't reopen STDOUT to $Logfile: $!");
	open(STDERR, '>>', $Logfile) or DaemonLog(1,"Couldn't reopen STDERR to $Logfile: $!");
}

# Purpose: Handler of SIGPIPE
sub SigPipeHandler
{
	DaemonLog(5,'Recieved SIGPIPE');
}

# Purpose: This is the main loop of the daemon. It should never return.
# Usage: MainLoop();
# 	The data should be loaded, notifications calculated and socket opened before running this
#    - to put it simply. Never run directly. Run DaemonInit();
sub MainLoop
{
	DaemonLog(5,'Entering main loop');
	while(true)
	{
		my $SleepTime = FindSleepDuration();
		DaemonLog(5,'Going to sleep for '.$SleepTime.' seconds');
		# Block until one handle is available or it times out
		my @Ready_Handles = $ConnectionSelector->can_read($SleepTime);
		# Timeout is true if no handle was processed
		my $Timeout = 1;
		DaemonLog(5,'Main loop processing');
		foreach my $Handle (@Ready_Handles)
		{
			# We didn't timeout
			$Timeout = 0;
			# If the handle is $ServerSocket then it's a new connection
			if ($Handle eq $ServerSocket)
			{
				my $NewClient = $ServerSocket->accept();
				$ConnectionSelector->add($NewClient);
				DaemonLog(4,'New handle '.$NewClient);
			} 
			# Handle isn't $ServerSocket, 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);
					DaemonLog(5,'Returning '.$Reply);
					print $Handle "$Reply\n";
				} 
				# If it isn't, then it closed the connection
				else
				{
					$ConnectionSelector->remove($Handle);
					DaemonLog(4,'Handle removed ',$Handle);
				}
			}
		}
		if($Timeout)
		{
			DaemonLog(5,'No handle had anything to say');
			if($NextEventIs eq 'DAYCHANGE')
			{
				DaemonLog(5,'Day change event');
				CalculateNotifications();
			}
			else
			{
				DaemonLog(5,'Notifier event');
				EventNotification($NextEventIs);
			}
		}
	}
}

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

# Purpose: Launch the notifier for events that has already occurred
# Usage: LaunchPrevNotifications();
sub LaunchPrevNotifications
{
	# First we create a list of UIDs
	my %LaunchUids;
	DaemonLog(5,'Preparing list of events that has occurred to notify the user about');
	foreach my $time (sort keys(%Notifications))
	{
		if(time() >= $time)
		{
			foreach my $UID (@{$Notifications{$time}})
			{
				$LaunchUids{$UID} = $time;
			}
		}
	}
	# And then we launch a notifier for each of them,
	# as long as the UID has not been set as "has notified"
	foreach my $UID(keys(%LaunchUids))
	{
		my $NoLaunch;
		foreach my $NotUID (split(/\s+/,$DaemonState{HasNotified}))
		{
			if($UID eq $NotUID)
			{
				DaemonLog(5,'Setting NoLaunch flag for '.$NotUID);
				$NoLaunch = true;
				last;
			}
		}
		if ($NoLaunch)
		{
			# Break out of the loop if, and ONLY IF, the event occurred more than one
			# hour ago.
			my $info = $iCalendar->get_info($UID);
			if (iCal_ConvertToUnixTime($info->{DTSTART}) < time() - 3600)
			{
				DaemonLog(5,'Keeping NoLaunch flag for '.$UID);
				next;
			}
			else
			{
				DaemonLog(5,'Ignoring NoLaunch flag for '.$UID.' due to it being less than one hour since it occurred');
			}
		}
		StartNotifier($UID);
	}
	# Update the state
	WriteDaemonState();
}

# Purpose: Initialize data paths
# Usage: InitDataPaths();
sub InitDataPaths
{
	# First find the conf dir
	if(not $DataDir)
	{
		$DataDir = DetectConfDir();
	}
	# Set the name as shown in ps ux
	my $DataName = $DataDir;
	$DataName =~ s/^$ENV{HOME}/~/;
	$0 .= ' ['.$DataName.']';
	# Set the path to the socket and logfile
	$SocketPath = $DataDir . '/' . $SocketName;
	$Logfile = $DataDir . '/' . 'daemon.log';
}

# Purpose: Perform initialization, then rest in the main loop
# Usage: InitDaemon();
sub InitDaemon
{
	DaemonLog(5,'Initializing');
	# Initialize data paths
	InitDataPaths();
	# Now try to open our socket. If that fails OpenSocket() will die() for us.
	OpenSocket();
	if ($ForceFork or not $NoFork)
	{
		Daemonize();
	}
	# Load our data
	LoadData();
	# Load the Day Planner config
	LoadDPConfig();
	# Calculate notifications
	CalculateNotifications();
	# Load the previous state information
	LoadDaemonState();
	# Launch notifiers for events that has already occurred but that the user has
	# not been notified about.
	LaunchPrevNotifications();
	# Okay, all initialiation has been done. Just rest in the main loop.
	MainLoop();
	# This should never happen
	die('FATAL ERROR: MainLoop() returned');
}

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('','',' (autdetected if not present)');
		PrintHelp('','--socketname','Sets the name of the socket in --dayplannerdir to');
		PrintHelp('','',' listen on. Default: '.$OrigSocketName);
		PrintHelp('-k','--kill','Shut down a currently running daemon');
		PrintHelp('-n','--nofork',"Don't fork, stay in the foreground");
		PrintHelp('-f','--force-fork','Force forking, overrides --nofork');
		PrintHelp('', '--version', 'Display version information and exit');
		PrintHelp('-h,', '--help', 'Display this help screen');
		PrintHelp('-v,', '--verbose', 'Be verbose. Supply several times to increase verbosity.');
		exit(0);
	},
	'v|verbose+' => \$LogLevel,
	'f|force-fork' => \$ForceFork,
	'n|nofork' => \$NoFork,
	'k|kill' => sub {
		$| = 1;
		print "Shutting down running daemon...";
		InitDataPaths();
		if (ShutdownRunning())
		{
			print "done";
		}
		else
		{
			print "none running";
		}
		print "\n";
		exit(0);
	},
	'dayplannerdir|d=s' => sub {
		if(not -e $_[1]) {
			die "$_[1] does not exist\n";
		}
		if(not -w $_[1]) {
			die "I can't write to $_[1]\n";
		}
		$DataDir = $_[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);
	},
	# For backwards compatibility. Ignored parameters.
	'D|V|o|output|debug|veryverbose' => sub {
		print "$_[0] has been deprecated and is no longer supported - ignored. Try -vvvvv\n";
	},
) or die "See $0 --help for more information\n";

InitDaemon();
