#!/usr/bin/perl
# dayplanner-daemon
# Copyright (C) Eskild Hustvedt 2007, 2008, 2009, 2012
#
# 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 };
# Constants for logging
use constant {
	DLOG_CRITICAL => 0,
	DLOG_ERROR => 1,
	DLOG_WARNING => 2,
	DLOG_NOTIFICATION => 3,
	DLOG_DEBUG => 4,
	DLOG_CALLTRACE => 5,
};
# 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/DP-iCalendar/lib/";
use lib "$FindBin::RealBin/modules/dayplanner/";
# External deps as fetched by the makefile
use lib "$FindBin::RealBin/modules/external/";
# Day Planner-specific libs
use DP::iCalendar qw(iCal_ConvertToUnixTime); # iCalendar support
use DP::GeneralHelpers qw(DPIntWarn DPIntInfo WriteConfigFile LoadConfigFile PrefixZero);
use DP::CoreModules;

# 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 => '',
);
my %HasNotified;

my %NotifierLaunchLock;

# 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.11';
# The version name. Used for GIT detection
my $VersionName = '2012';
# 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 default log level - increased for GIT
my $LogLevel = 2;
if ($VersionName eq 'GIT')
{
	$LogLevel = 4;
}
# The log file
my $Logfile;

# Signal handlers
$SIG{INT} = sub { Shutdown('SIGINT');};
$SIG{TERM} = sub { Shutdown('SIGTERM');};

# ----
# 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);
	}
	return $return;
}

# Purpose: Output log information.
# Usage: DaemonLog(LEVEL,Message);
# 	LEVEL is one of: DLOG_CRITICAL DLOG_ERROR DLOG_WARNING DLOG_NOTIFICATION
# 	DLOG_DEBUG DLOG_CALLTRACE
sub DaemonLog
{
	my $Level = shift;
	return if not($Level <= $LogLevel);
	my $Message = shift;
	my $MsgPrefix;
	if($Level == DLOG_CRITICAL or $Level == DLOG_ERROR)
	{
		$MsgPrefix = 'Error:        ';
	}
	elsif($Level == DLOG_WARNING)
	{
		$MsgPrefix = 'Warning:      ';
	}
	elsif($Level == DLOG_NOTIFICATION)
	{
		$MsgPrefix = 'Notification: ';
	}
	elsif($Level == DLOG_DEBUG)
	{
		$MsgPrefix = 'Debug:        ';
	}
	elsif($Level == DLOG_CALLTRACE)
	{
		$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;
	$lmon = "0$lmon" unless $lmon >= 10;
	$lmday = "0$lmday" unless $lmday >= 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;
	assert(defined $Events_NotifyPre);

	my $Number = $Events_NotifyPre;
	my $Type = $Events_NotifyPre;
	my $Total = 0;
	$Number =~ s/^(\d+).*$/$1/;
	if ($Number =~ /\D/)
	{
		DaemonLog(DLOG_CRITICAL,'The Events_NotifyPre configuration option is invalid, I was unable to parse it. Pre-event notifications won\'t work!');
		DaemonLog(DLOG_DEBUG,'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(DLOG_CRITICAL,'The Events_NotifyPre configuration option is invalid, I was unable to parse it. Pre-event notifications won\'t work!');
			DaemonLog(DLOG_DEBUG,'Unable to parse number: '.$Number);
		}
	}
	DaemonLog(DLOG_CALLTRACE,'ParseNotifyPre: '.$Total);
	return($Total);
}

# Purpose: Die with useful information if an Assertion fails
# Usage: assert(TRUE/FALSE EXPR, REASON);
sub assert
{
	my $expr = shift;
	return true if $expr;
	my ($package, $filename, $line, $subroutine, $hasargs, $wantarray, $evaltext, $is_require, $hints, $bitmask) = caller(1);
	my ($s2_package, $s2_filename, $s2_line, $s2_subroutine, $s2_hasargs, $s2_wantarray, $s2_evaltext, $s2_is_require, $s2_hints, $s2_bitmask) = caller(0);
	my $msg;
	if(defined($_[0]))
	{
		$msg = "Assertion failure at $s2_filename:$s2_line in $subroutine originating from call at $filename:$line: $_[0]\n";
	}
	else
	{
		$msg = "Assertion failure at $s2_filename:$s2_line in $subroutine originating from call at $filename:$line\n";
	}
	# If this is the GIT version or DP_FATAL_ASSERT is set, then the error is fatal.
	if ($VersionName eq 'GIT' or (defined($ENV{DP_FATAL_ASSERT}) and $ENV{DP_FATAL_ASSERT} eq 1))
	{
		die($msg);
	}
	# If not, then we output it and happily continue processing
	else
	{
		DaemonLog(DLOG_CRITICAL,$msg);
	}
	return true;
}

# Purpose: Check if two unixtimes are on the same date
# Usage: OnSameDate(TIME,TIME);
# 	Returns bool
sub OnSameDate
{
	my $Time1 = shift;
	my $Time2 = shift;
	assert(defined $Time1, 'time1 is undef');
	assert(defined $Time2, 'time2 is undef');
	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: Write the daemon state
# Usage: WriteDaemonState();
sub WriteDaemonState
{
	DaemonLog(DLOG_CALLTRACE,'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\n# If you do, and the daemon is running it will overwrite your changes.",
		HasNotified => 'This is a list of UIDs that has already been notified *today*',
		LastWritten => 'The unix time of when this file was last written ('.scalar(localtime()).')',
	);
	# TODO: DROP
	if(OnSameDate(time(),$DaemonState{LastWritten}))
	{
		# Generate HasNotified
		$DaemonState{HasNotified} .= $_ .' ' foreach(keys(%HasNotified));
	}
	else
	{
		DaemonLog(DLOG_DEBUG,'HasNotified= was not from today. Replacing with an empty string');
		delete($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(DLOG_CALLTRACE,'Loading daemon state from '.$DataDir . '/daemon_state.conf');
		LoadConfigFile($DataDir . '/daemon_state.conf', \%DaemonState, undef, 0);
	} 
	else
	{
	}
	if(OnSameDate(time(),$DaemonState{LastWritten}))
	{
		DaemonLog(DLOG_CALLTRACE,'HasNotified was from today. Keeping it');
		foreach(split(/\s+/,$DaemonState{HasNotified}))
		{
			$HasNotified{$_} = true;
		}
	}
	else
	{
		DaemonLog(DLOG_CALLTRACE,'HasNotified was not from today, so skipping it');
	}
	delete($DaemonState{HasNotified});
	WriteDaemonState();
}

# Purpose: Add a new UID that has been notified
# Usage: AddNotifiedUID(UID);
sub AddNotifiedUID
{
	my $UID = shift;
	assert(defined $UID);
	# Clear HasNotified if it's old
	if(not OnSameDate(time(),$DaemonState{LastWritten}))
	{
		DaemonLog(DLOG_CALLTRACE,'Clearing old HasNotified');
		%HasNotified = ();
		# Set LastWritten, so that we don't re-clear it again if more UIDs are added
		# before the next state write.
		$DaemonState{LastWritten} = time();
	}
	DaemonLog(DLOG_CALLTRACE, 'Marking UID '.$UID.' as notified');
	$HasNotified{$UID} = true;
}

# 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(DLOG_CALLTRACE,"PreNotDay = $PreNotValues{PreNotDay} and PreNotSeconds = $PreNotValues{PreNotSeconds}");
}

# Purpose: Print formatted --help output
# Usage: PrintHelp("-shortoption", "--longoption", "description");
#  Description will be reformatted to fit within a normal terminal
sub PrintHelp {
	# The short option
	my $short = shift,
	# The long option
	my $long = shift;
	# The description
	my $desc = shift;
	# The generated description that will be printed in the end
	my $GeneratedDesc;
	# The current line of the description
	my $currdesc = '';
	# The maximum length any line can be
	my $maxlen = 80;
	# The length the options take up
	my $optionlen = 20;
	# Check if the short/long are LONGER than optionlen, if so, we need
	# to do some additional magic to take up only $maxlen.
	# The +1 here is because we always add a space between them, no matter what
	if ((length($short) + length($long) + 1) > $optionlen)
	{
		$optionlen = length($short) + length($long) + 1;
	}
	# Split the description into lines
	foreach my $part (split(/ /,$desc))
	{
		if(defined $GeneratedDesc)
		{
			if ((length($currdesc) + length($part) + 1 + 20) > $maxlen)
			{
				$GeneratedDesc .= "\n";
				$currdesc = '';
			}
			else
			{
				$currdesc .= ' ';
				$GeneratedDesc .= ' ';
			}
		}
		$currdesc .= $part;
		$GeneratedDesc .= $part;
	}
	# Something went wrong
	die("Option mismatch") if not $GeneratedDesc;
	# Print it all
	foreach my $description (split(/\n/,$GeneratedDesc))
	{
		printf "%-4s %-15s %s\n", $short,$long,$description;
		# Set short and long to '' to ensure we don't print the options twice
		$short = '';$long = '';
	}
	# Succeed
	return true;
}

# Purpose: system() replacement that logs the command run
# Usage: loggingSystem(SAME_AS_system());
sub loggingSystem
{
	assert(scalar(@_) > 0);
	DaemonLog(DLOG_CALLTRACE,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;
	assert(defined $time);
	assert($time =~ /:/);
	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
{
	my $isReload = false;
	if($iCalendar)
	{
		# If the object already exists we just overwrite it
		# with a new one.
		$isReload = true;
		undef $iCalendar;
		DaemonLog(DLOG_CALLTRACE,'Data reload requested');
	}
	$iCalendar = DP::iCalendar->new($DataDir.'/'.$CalendarFile);
	if ($iCalendar)
	{
		DaemonLog(DLOG_CALLTRACE,'Data loaded from ' .$DataDir.'/'.$CalendarFile);
	}
	else
	{
		$iCalendar = DP::iCalendar->newfile($DataDir.'/'.$CalendarFile);
		assert($iCalendar);
		DaemonLog(DLOG_NOTIFICATION, 'Data file did not exist. Called ->newfile('.$DataDir.'/'.$CalendarFile.');');
	}
	# If we're in reload mode we need to calculate the notifications.
	# If we're not in reload mode then the required info for calculation
	# has not yet been loaded, so it will be called later.
	if ($isReload)
	{
		CalculateNotifications();
	}
}

# Purpose: Calculate or recalculate the list of notification events
# Usage: CalculateNotifications();
sub CalculateNotifications
{
	DaemonLog(DLOG_CALLTRACE,'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(REASON?);
sub Shutdown
{
	my $reason = shift;
	if ($reason)
	{
		DaemonLog(DLOG_DEBUG,'Shutting down: '.$reason);
	}
	else
	{
		DaemonLog(DLOG_DEBUG,'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();
	# Purge messages
	$| = 1;
	$| = 0;
	# Close STDOUT (which is the log target, an additional precaution to ensure purging)
	close(STDOUT);
	# 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(DLOG_CALLTRACE,'Sleeptime - is tomorrow, going out of loop: '.$time);
			last;
		}
		# If $time is more than current time then return that
		if($time > time())
		{
			DaemonLog(DLOG_CALLTRACE,'Found sleeptime - until: '.$time);
			$NextEventIs = $time;
			return($time - time());
		}
	}
	DaemonLog(DLOG_CALLTRACE,'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";
	assert(defined $ServerSocket);
	# Enforce strict permissions
	chmod(oct(600),$SocketPath);
	# Trap SIGPIPE
	$SIG{PIPE} = \&SigPipeHandler;
	# Create a new select handle for reading
	$ConnectionSelector = IO::Select->new();
	assert(defined $ConnectionSelector);
	# Add the main server
	$ConnectionSelector->add($ServerSocket);

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

# Purpose: Handle incoming daemon commands
# Usage: $Return = CommandHandler(LINE);
sub CommandHandler 
{
	$_ = shift;
	DaemonLog(DLOG_CALLTRACE,$_);
	my $PID = $_;
	$PID =~ s/^(\d+)\s+(.*)/$1/;
	if(not $PID or $PID =~ /\D/)
	{
		DaemonLog(DLOG_NOTIFICATION,"Malformed request: $_");
		return('ERR MALFORMED_REQUEST');
	}
	if ($PID == $$)
	{
		DaemonLog(DLOG_DEBUG,"Strange, I seem to have sent myself a command ($_). This shouldn't happen, but letting it proceed");
	}
	study();
	s/^(\d+)\s+//;
	if(/^RELOAD_DATA/)
	{
		LoadData();
		return('done');
	}
	elsif(/^RELOAD_CONFIG/)
	{
		LoadDPConfig();
		return('done');
	}
	elsif(/^SHUTDOWN/)
	{
		Shutdown("Requested by $PID");
	}
	elsif (/^VERSION/)
	{
		return($Version);
	}
	elsif(/^PING/)
	{
		return('PONG');
	}
	elsif(s/^SET_LOGLEVEL\s+(\d+)/$1/)
	{
		s/\s//g;
		if (/\D/) {
			DaemonLog(DLOG_NOTIFICATION,"Got request to set loglevel to an invalid value ('$_' from $PID)");
			return('ERR # SYNTAX ERROR');
		}
		$LogLevel = $_;
		DaemonLog(DLOG_DEBUG,"$$ told me to set LogLevel to $_, so I did");
		return('done');
	}
	# Old API
	elsif(/^(HI|BYE|NOTIFICATION|DEBUG|GET_PATH)/)
	{
		DaemonLog(DLOG_NOTIFICATION,'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
	{
		DaemonLog(DLOG_DEBUG,"Invalid command ($_) recieved from $PID");
		return('ERR INVALID_COMMAND');
	}
	
}

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

# Purpose: Start the notifier
# Usage: StartNotifier(UID);
sub StartNotifier
{
	my $UID = shift;
	assert(defined $UID);
	# The current time, used for launch locking
	my $time = time();
	# Check for a launch lock
	if ($NotifierLaunchLock{$UID})
	{
		# Within a 4 minute timeframe we shouldn't re-launch the notifier for the same event.
		# If that happens, scream in agony and refuse to cooperate.
		my $lowtime = $NotifierLaunchLock{$UID} - 120;
		my $hightime = $NotifierLaunchLock{$UID} + 120;
		if (not $lowtime > $time and not $hightime < $time)
		{
			DaemonLog(DLOG_ERROR,'Attempted to run notifier for '.$UID.' more than once. This is a bug, refusing to launch it.');
			return true;
		}
	}
	DaemonLog(DLOG_DEBUG,'Starting notifier for UID '.$UID);
	# Set a launch lock
	$NotifierLaunchLock{$UID} = time();
	# Go through the path and locate a notifier
	foreach(split(/:/, sprintf('%s:%s', $ENV{PATH}, dirname(Cwd::realpath($Self)))))
	{
		# If it is executeable try to run it
		if ( -x $_.'/'.$NotifierName)
		{
			# Try to launch it, whine if it doesn't work
			if(loggingSystem($_.'/'.$NotifierName, '--calendar', $DataDir .'/'.$CalendarFile, '--uid', $UID)  == 0)
			{
				AddNotifiedUID($UID);
				return(true);
			}
			else
			{
				DaemonLog(DLOG_WARNING,'Tried running notifier at '.$_.'/'.$NotifierName.' but it exited with a nonzero return value. Continuing search');
			}
		}
		else
		{
			DaemonLog(DLOG_CALLTRACE,'Found nonexecuteable notifier at '.$_.'/'.$NotifierName.'.');
		}
	}
}

# 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(DLOG_ERROR,"Unable to start a new POSIX session (setsid()): $!");
	# Change dir to / - this to avoid clogging up a mountpoint
	chdir('/') or DaemonLog(DLOG_ERROR,"Unable to chdir to /: $!");
	# (We finish the daemonizing after loading the config and calendar)
	open(STDIN, '<', '/dev/null') or DaemonLog(DLOG_ERROR,"Couldn't reopen STDIN to /dev/null: $!");
	open(STDOUT, '>>', $Logfile) or DaemonLog(DLOG_ERROR,"Couldn't reopen STDOUT to $Logfile: $!");
	open(STDERR, '>>', $Logfile) or DaemonLog(DLOG_ERROR,"Couldn't reopen STDERR to $Logfile: $!");
}

# Purpose: Handler of SIGPIPE
# Usage: SIG{PIPE} = \&SigPipeHandler;
sub SigPipeHandler
{
	DaemonLog(DLOG_CALLTRACE,'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(DLOG_DEBUG,'Day Planner Daemon version '. $Version.' entering main loop');
	while(true)
	{
		my $SleepTime = FindSleepDuration();
		DaemonLog(DLOG_CALLTRACE,'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(DLOG_CALLTRACE,'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(DLOG_DEBUG,'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(DLOG_CALLTRACE,'Returning '.$Reply);
					print $Handle "$Reply\n";
				} 
				# If it isn't, then it closed the connection
				else
				{
					$ConnectionSelector->remove($Handle);
					DaemonLog(DLOG_DEBUG,'Handle removed ',$Handle);
				}
			}
		}
		if($Timeout)
		{
			DaemonLog(DLOG_CALLTRACE,'No handle had anything to say');
			if($NextEventIs eq 'DAYCHANGE')
			{
				DaemonLog(DLOG_CALLTRACE,'Day change event');
				CalculateNotifications();
			}
			else
			{
				DaemonLog(DLOG_CALLTRACE,'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(DLOG_CALLTRACE,'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;
		if ($HasNotified{$UID})
		{
			DaemonLog(DLOG_CALLTRACE,'Setting NoLaunch flag for '.$UID);
			$NoLaunch = true;
		}
		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(DLOG_CALLTRACE,'Keeping NoLaunch flag for '.$UID);
				next;
			}
			else
			{
				DaemonLog(DLOG_CALLTRACE,'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(DLOG_CALLTRACE,'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
	assert(true, '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 (autodetected if not present).');
		PrintHelp('','--socketname','Sets the name of the socket in --dayplannerdir to listen on. Default: '.$OrigSocketName);
		PrintHelp('','--replace','Replace the running daemon (if any) with this one');
		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('-t','--test','Use a seperate debug/ configuration directory');
		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);
	},
	'replace' => sub {
		InitDataPaths();
		ShutdownRunning();
	},
	'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";
		exit(0);
	},
	'test|t:s' => sub {
		shift;
		my $prefix = shift;
		if ($prefix && $prefix =~ /(\s|\/)/)
		{
			die("The parameter for --test needs to be a string with no spaces and no /\n");
		}
		elsif(not defined $prefix)
		{
			$prefix = '';
		}
		elsif ($prefix =~ /\D/)
		{
			$prefix = '_'.$prefix;
		}
		$DataDir = DetectConfDir();
		$DataDir .= "/debug$prefix";
		my $Dir = $DataDir;
		$Dir =~ s/^$ENV{HOME}/~/g;
		DPIntInfo("Running in test mode (using $Dir)");
	},
	# 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();
__END__
=head1 NAME

Day Planner daemon - reminding daemon for Day Planner.

=head1 SYNOPSIS

B<dayplanner-daemon> [I<OPTIONS>]

=head1 DESCRIPTION

This is the Day Planner daemon. It is started by the main L<Day
Planner|dayplanner(1)> program and it is almost never neccesary to call it
manually.

It calls I<dayplanner-notifier(1)> to display the notifications to the user.

=head1 OPTIONS

=over

=item B<-h, --help>

Display the help screen.

=item B<-v, --version>

Display version information.

=item B<-t, --test> I<N>

Start in test mode. See the --test documentation in L<dayplanner(1)>.

=item B<--dayplannerdir> I<DIR>

Use the directory I<DIR> instead of the default Day Planner configuration
directory. See also I<--test> and I<--confdir> in L<dayplanner(1)>.

=item B<-k, --kill>

Shut down the currently running daemon.

=item B<-n, --nofork> 

Don't fork. Stay in the foreground.

=item B<-f, --force-fork>

Force forking. Overrides --nofork.

=item B<-v, --verbose>

Be verbose. Supply multiple times to increase verbosity.
This goes to the daemon log if I<--nofork> was not supplied.

=back

=head1 HELP/SUPPORT

See L<http://www.day-planner.org/index.php/help>

=head1 AUTHOR

Eskild Hustvedt I<<eskild
at zerodogg
dot
org>>

=head1 FILES

See the FILES section in L<dayplanner(1)>

=head1 SEE ALSO

L<dayplanner(1)> L<dayplanner-notifier(1)>

=head1 LICENSE AND COPYRIGHT

Copyright (C) Eskild Hustvedt 2007-2012

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 L<http://www.gnu.org/licenses/>.
