#!/usr/bin/perl
# Day Planner
# A graphical Day Planner written in perl that uses Gtk2
# Copyright (C) Eskild Hustvedt 2006, 2007, 2008
# $Id: dayplanner 1928 2008-01-13 11:42: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 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/>.

# vim: set tabstop=4 shiftwidth=4 :

require 5.6.0;			# We need perl 5.6.0 at the very least
						# and even this is mostly untested.
use strict;				# Force strict coding
use warnings;			# Tell perl to warn about things
use POSIX;				# Uses the various time/date functions from POSIX
use Gtk2;				# Use Gtk2 :)
use Gtk2::SimpleList;	# We use Gtk2::SimpleList to create the eventlist
use Gtk2::Gdk::Keysyms;	# Easier keybindings
use MIME::Base64;		# Used for DPS-related functions. Needed even when DPS is diasbled. 
use Getopt::Long;		# Commandline options
use FindBin;			# So that we can detect module dirs during runtime
# Useful constants for prettier code
use constant { true => 1, false => 0 };
# This here is done so that we can use local versions of our libs
use lib "$FindBin::RealBin/modules/Date-HolidayParser/lib/";
use lib "$FindBin::RealBin/modules/DP-iCalendar/lib/";
use lib "$FindBin::RealBin/modules/DP-GeneralHelpers/lib/";
use lib "$FindBin::RealBin/modules/";
# Day Planner-specific libs
use Date::HolidayParser 0.4;	# Parsing of .holiday files
use DP::iCalendar qw(iCal_ParseDateTime iCal_GenDateTime); # iCalendar support
use DP::iCalendar::Manager;		# Support for multiple iCalendars using a single object
use DP::GeneralHelpers qw(DPIntWarn DPIntInfo WriteConfigFile LoadConfigFile AppendZero);
use DP::GeneralHelpers::IPC;
use DP::GeneralHelpers::I18N;

# Scalars
our $Version = '0.8.1';
my $VersionName = 'Dark Dictionary';
my $RCSRev = '$Id: dayplanner 1928 2008-01-13 11:42:15Z zero_dogg $';
my $SaveToDir;						# The configuration and eventlist directory (set later)
my $HolidayFile;					# The file to load holiday definitions from (set later)
my $ICSFile = 'calendar.ics';		# The ICS calendar
my $ConfigFile = 'dayplanner.conf';	# The filename to save the configuration to
my $DaemonName = 'dayplanner-daemon';# The name of the daemon
my $DaemonInitialized = 0;			# Has the daemon been initialized?
my $DaemonSocketName = 'Daemon_Socket';# The name of the daemon socket
my $DaemonSocket;					# The variable to connect to
my $i18n;
my $DPS_APILevel = '06';			# The DPS API level used/supported
my $iCalendar;						# The DP::iCalendar::Manager object.
my $IPC_Socket;
my $HTML_PHP;

# FIXME: Unused
# Arrays
my @SaveFallbackDirs = (				# The directories to use for fallback saving if we can't save to $SaveToDir
	$ENV{HOME}, "$ENV{HOME}/Desktop", "$ENV{HOME}/Documents", "$ENV{HOME}/tmp", "/tmp", "/var/tmp", "/usr/tmp",
);

# Hashes
my %Holidays;			# The holidays
my %InternalConfig;		# Internal configuration values
my %UserConfig;			# User-selected configuration values
my %DPServices;			# Day Planner services hash
my %RuntimeModules;		# Keeps track of modules loaded during runtime

# Gtk2 objects
my (
	$CalendarWidget,	$EventlistWidget,	$WorkingAreaHBox,
	$EventlistWin,		$MainWindow,		$Toolbar,
	$ToolbarEditButton,	$MenuEditEntry,		$MenuDeleteEntry,
	$UpcomingEventsBuffer,	$UpcomingEventsWidget,	$ToolbarDeleteButton,
);	# Gtk objects

my $HolidayParser;	# The Date::HolidayParser object
my $Gtk2Init;		# Info about if gtk2 is initialized or not
my $ShutdownDaemon;	# If we should shut down the daemon on exit

# Window state
($InternalConfig{MainWin_Width}, $InternalConfig{MainWin_Height}) = (600,365);	# Default size is 600x365 - overridden by state.conf

# Set up signal handlers
$SIG{INT} = \&DP_SigHandler;
$SIG{TERM} = \&DP_SigHandler;

# =============================================================================
# CORE FUNCTIONS
# =============================================================================

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# I18n functions (Day Planner Locale::gettext wrapper)
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Initialize the i18n subsystem
# Usage: DP_InitI18n();
sub DP_InitI18n {
	$i18n = DP::GeneralHelpers::I18N->new();
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Daemon communication functions
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Initialize and connect to the daemon
# Usage: DaemonInit();
sub DaemonInit {
	$DaemonInitialized = 1;
	if (-e "$SaveToDir/$DaemonSocketName") {
		$DaemonSocket = DP::GeneralHelpers::IPC->new_client("$SaveToDir/$DaemonSocketName",sub { return });
		# TODO: Implement a IPC->blocking_client_send() and use that to check daemon VERSION
	}
	if (not $DaemonSocket) {
		if(not StartDaemon()) {
			return(undef);
		}
		return(DaemonInit());
	}
	return(true);
}

# Purpose: Start the Day Planner daemon
# Usage: StartDaemon();
sub StartDaemon {
	foreach(split(/:/, sprintf('%s:%s', $FindBin::RealBin, $ENV{PATH} ))) {
		if (-x "$_/$DaemonName") {
			# 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("$_/$DaemonName", '--force-fork', '--dayplannerdir', $SaveToDir)) {
				return(1);
			}
		}
	}
	DPIntWarn('Unable to start daemon!');
	return(0);
}

# Purpose: Close the connection to the daemon (shutdown if $ShutdownDaemon) and close the IPC socket
# Usage: CloseIPC();
sub CloseIPC {
	if($ShutdownDaemon) {
		$DaemonSocket->client_send($$.' SHUTDOWN');
	}
	if($IPC_Socket) {
		$IPC_Socket->destroy();
	}
	if($DaemonSocket) {
		$DaemonSocket->destroy();
	}
	return(1);
}

# Purpose: Send a message to the daemon
# Usage: Daemon_SendData(DATA);
sub Daemon_SendData {
	if(not $DaemonSocket) {
		return(undef);
	}
	$DaemonSocket->client_send("$$ $_[0]");
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Services layer
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Return better errors than IO::Socket::SSL does.
# Usage: my $ERROR = IO_Socket_INET_Errors($@);
#	Errors:
#		OFFLINE = Network is unreachable
#		REFUSED = Connection refused
#		BADHOST = Bad hostname (should often be handled as OFFLINE)
#		TIMEOUT = The connection timed out
#		* = Anything else simply returns $@
sub IO_Socket_INET_Errors {
	my $Error = shift;
	if($Error =~ /Network is unreachable/i) {
		return('OFFLINE');
	} elsif ($Error =~ /Bad hostname/i) {
		return('BADHOST');
	} elsif ($Error =~ /Connection refused/i) {
		return('REFUSED');
	} elsif ($Error =~ /timeout/i) {
		return('TIMEOUT');
	} else {
		return($Error);
	}
}

# Purpose: Output an error occurring with DPS
# Usage: DPS_Error(User_Error, Technical_Error)
#	User_Error is displayed as a pop-up error dialog.
#	Technical_Error is DPIntWarn()ed, it is optional.
#	If no technical_error is supplied then User_error is used.
sub DPS_Error {
	my $user_error = shift;
	# Tech_error is set to user_error when not supplied
	my $tech_error = $_[0] ? $_[0] : $user_error;
	DPIntWarn("DPS: $tech_error");
	DPS_Log($tech_error);
	if(defined($user_error)) {
		$DPServices{Error} = $user_error;
	}
}

# Purpose: Set the status in the DPS GUI Window
# Usage: DPS_Status(TEXT, COMPLETED);
#	COMPLETED is a number between 0 and 1 (such as 0.1, 0.5)
#	0 = 0%
#	1 = 100%
sub DPS_Status {
	my ($Text, $Completed) = @_;
	return unless($Gtk2Init);
	if(defined($DPServices{ProgressWin})) {
		$DPServices{ProgressWin}->{ProgressBar}->set_fraction($Completed);
		$DPServices{ProgressWin}->{ProgressBar}->set_text($Text);
		Gtk2->main_iteration while Gtk2->events_pending;
	}
}

# Purpose: Upload data to a Day Planner services server
# Usage: DPS_Upload();
sub DPS_Upload {
	my $LastMD5 = $InternalConfig{DPS_LastMD5} ? $InternalConfig{DPS_LastMD5} : "undef";
	my $SendData = encode_base64($iCalendar->get_rawdata(),'');
	chomp($SendData);
	my $MD5 = md5_base64($SendData);
	my $Reply = DPS_DataSegment("SENDDATA $MD5 $LastMD5 $SendData 0");
	if(not $Reply eq 'OK') {
		# TODO: These need cleaning
		if($Reply =~ s/^ERR\s+(.*)$/$1/) {
			DPS_Error($i18n->get('An error ocurred while uploading the data'), 'An error ocurred during upload of the data: ' . $Reply);
		} elsif($Reply =~ /^EXPIRED/) {
			DPS_Error($i18n->get('Your account has expired. If you are using a paid service this may be because you have not paid for the current period. If not, you should contact your service provider to get information on why your account has expired.'), 'Account expired');
		} else {
			# Sending the data failed
			DPS_Error($i18n->get_advanced('The server did not accept the uploaded data and replied with an unknown value: %(value)', { value => $Reply }));
		}
		return(undef);
	}
	# We successfully uploaded the data. So set DPS_LastMD5 and return true
	$InternalConfig{DPS_LastMD5} = $MD5;
	return(1);
}

# Purpose: Download data from a Day Planner services server
# Usage: DPS_Download(MERGE?);
#	If MERGE is true then it will not overwrite the current data
#	with the downloaded data, but rather use the DP::iCalendar merge
#	function to merge it into place.
# This function itself is stupid, it doesn't know about MD5 sums of local data
# or anything. It will download the data no matter what, it is up to the caller
# to check if we actually need to download data or not.
sub DPS_Download {
	my $Merge = shift;
	my $Data = DPS_DataSegment('GETDATA');
	if($Data =~ /^OK/) {
		DPS_Log('Downloaded data');
		my $Initial = $Data;
		my $MD5 = $Data;
		my $MainData = $Data;
		$Initial =~ s/^(\S+)\s+.*$/$1/;
		$MD5 =~ s/^(\S+)\s+(\S+)\s+.*/$2/;
		if(not $MainData =~ s/^(\S+)\s+(\S+)\s+(\S+)\s*$/$3/)
		{
			# FIXME: Rewrite this
			DPS_Error("FATAL: UNABLE TO GRAB DATA. DUMPING DATA:\nData recieved: $Initial");
		}
		elsif(not md5_base64($MainData) eq $MD5)
		{
			# FIXME: Rewrite this
			DPS_Error($i18n->get('The data became corrupted during download. You may want to attempt to synchronize again.'),'MD5 mismatch during download: got ' . md5_base64($MainData) . ' expected ' . $MD5);
		} 
		else
		{
			# Decode the base64 encoded data
			$MainData = decode_base64($MainData);
			# Remove junk and populate @DataArray
			my @DataArray;
			$MainData =~ s/\r//g;
			push(@DataArray, $_) foreach(split(/\n/,$MainData));
			$MainData = undef;
			# If we're in merge mode then enable SMART MERGE and then add
			# the file, if not then clean and add the file
			my $iCalendarMain = $iCalendar->get_primary();
			if($Merge)
			{
				$iCalendarMain->enable('SMART_MERGE');
				$iCalendarMain->addfile(\@DataArray);
				$iCalendarMain->disable('SMART_MERGE');
			} 
			else 
			{
				$iCalendarMain->clean();
				$iCalendarMain->addfile(\@DataArray);
			}
			# Download succesful. Set DPS_LastMD5
			$InternalConfig{DPS_LastMD5} = $MD5;
			UpdatedData();
			return(1);
		}
	} else {
		DPS_Log("Unable to download data. Server replied: $Data");
	}
	# If we got this far then it means we failed.
	return(undef);
}

# Purpose: Synchronize our local data with the server data
# Usage: DPS_DataSync();
sub DPS_DataSync {
	DPS_Status($i18n->get('Synchronizing'),0.2);
	# Get information we need
	#	The server's data MD5 sum
	my $ServerMD5 = DPS_DataSegment('GET_MD5');
	#	The MD5 sum of our current local data
	my $LocalMD5 = md5_base64(encode_base64($iCalendar->get_rawdata(),""));
	#	The MD5 sum of the data we last uploaded
	my $LastUpMD5 = $InternalConfig{DPS_LastMD5};

	# Okay, the required information is available.
	# First check if our current local MD5 sum matches the one on the server.
	# If it does then we return without doing anything at all.
	if(defined($LocalMD5) and defined($ServerMD5) and $ServerMD5 eq $LocalMD5) {
		DPS_Log("ServerMD5[$ServerMD5] matched our LocalMD5[$LocalMD5]. Not doing anything");
		return(1);
	} 
	# It didn't match. So now we check if our last uploaded MD5 matches the servers MD5.
	# If it does then we just upload.
	elsif ($ServerMD5 eq $LastUpMD5 or $ServerMD5 eq '[NONE]') {
		DPS_Log("Local data changed, uploading to DPS (local MD5 is $LocalMD5 and the servers MD5 is $ServerMD5)");
		DPS_Status($i18n->get('Synchronizing'),0.4);
		my $Return = DPS_Upload();
		DPS_Status($i18n->get('Synchronizing'),0.8);
		UpdatedData();
		DPS_Status($i18n->get('Synchronizing'),0.9);
		return($Return);
	}
	# That didn't match either. So we check if our local MD5 is identical to the
	# last uploaded MD5. If that is the case then we just need to download the
	# data from the server.
	elsif ($LastUpMD5 eq $LocalMD5) {
		DPS_Log('Remote data changed, downloading from DPS');
		DPS_Status($i18n->get('Synchronizing'),0.5);
		my $Return = DPS_Download();
		DPS_Status($i18n->get('Synchronizing'),0.8);
		UpdatedData();
		DPS_Status($i18n->get('Synchronizing'),0.9);
		return($Return);
	}
	# Okay, nothing matched. This means that we have one local MD5 sum,
	# one remote MD5 sum and one "local last uploaded" MD5 sum - and they all differ.
	# We must here download the data from the server, merge it with our own and then
	# upload the new data.
	else {
		DPS_Log('Both remote and local data has changed. Downloading, merging and reuploading');
		DPS_Status($i18n->get('Synchronizing'),0.3);
		if(DPS_Download(1)) {
			DPS_Status($i18n->get('Synchronizing'),0.6);
			my $Return = DPS_Upload();
			DPS_Status($i18n->get('Synchronizing'),0.8);
			UpdatedData();
			DPS_Status($i18n->get('Synchronizing'),0.9);
			return($Return);
		}
	}
}

# Purpose: Log DPS info
# Usage: DPS_Log(INFO);
sub DPS_Log {
	if(defined($DPServices{Log_FH})) {
		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;
		my $FH = $DPServices{Log_FH};
		print $FH "[$lhour:$lmin:$lsec] $_[0]\n";
	}
	return(1);
}

# Purpose: High-level API to DPS
# Usage: DPS_Perform(FUNCTION);
#	FUNCTION can be one of:
#	SYNC
#	...
sub DPS_Perform {
	runtime_use('Digest::MD5 qw(md5_base64 md5_hex)');
	# The function we are going to perform
	my $Function = shift;
	# A coderef to the code which we need to run to close the GUI
	# dialogues used.
	my $GuiEnded = sub {
		return unless($Gtk2Init);
		$DPServices{ProgressWin}->{Window}->destroy();
		if(defined($DPServices{Error})) {
			DPError($i18n->get_advanced("An error occurred with the Day Planner services:\n\n%(error)",{ error => $DPServices{Error}}));
			delete($DPServices{Error});
		}
		delete($DPServices{ProgressWin});
		$MainWindow->set_sensitive(1);
	};

	# Make sure that DPS is enabled in the config
	return unless(defined($UserConfig{DPS_enable}) and $UserConfig{DPS_enable} eq "1");
	# Check if the user wants to temporarily disable DPS
	return if(defined($ENV{DP_DISABLE_SERVICES}) and $ENV{DP_DISABLE_SERVICES} eq "1");
	# Verify that all required options are set in the config
	foreach my $Option (qw(host port user pass)) {
		unless(defined($UserConfig{"DPS_$Option"}) and length($UserConfig{"DPS_$Option"})) {
			DPIntWarn("DPS enabled but the setting DPS_$Option is missing. Disabling.");
			$UserConfig{DPS_enable} = 0;
			return(undef);
		} else {
			$DPServices{$Option} = $UserConfig{"DPS_$Option"};
		}
	}
	return if not DPS_SSLSocketTest();
	# Create the progress window
	if($Gtk2Init) {
		$MainWindow->set_sensitive(0);
		$DPServices{ProgressWin} = DPCreateProgressWin($i18n->get('Services'), $i18n->get('Initializing'));
	}
	# Open up the logfile if it isn't open. This should be left open for the
	# entirety of the DPS session.
	if(not defined($DPServices{Log_FH})) {
		open($DPServices{Log_FH}, '>', "$SaveToDir/services.log");
		chmod(oct(600),"$SaveToDir/services.log");
		DPS_Log('DPS initialized');
	}
	DPS_Status($i18n->get('Connecting'),0);
	# Connect to the server, if this fails then we return undef without doing anything
	if(not DPS_Connect()) {
		$GuiEnded->();
		return(undef);
	}
	DPS_Status($i18n->get('Connected'),0.1);
	# Connection established, process $Function
	if($Function eq 'SYNC') {
		DPS_DataSync();
	} else {
		DPIntWarn("DPS_Perform($Function) called, but the function \"$Function\" is unknown.");
	}
	DPS_Status($i18n->get('Complete'),1);
	# Disconnect and return
	DPS_Disconnect();
	$GuiEnded->();
	return(1);
}

# Purpose: Test for IO::Socket::SSL
# Usage: DPS_SSLSocketTest();
sub DPS_SSLSocketTest {
	# Make sure the IO::Socket::SSL module is available and loaded
	if(not runtime_use('IO::Socket::SSL',true)) {
		if (not $DPServices{IO_SOCKET_SSL_ERR_DISPLAYED}) {
			DPError($i18n->get("You don't have the IO::Socket:SSL module. This module is required for the Day Planner services to function. The services will not function until this module is installed."));
			$DPServices{IO_SOCKET_SSL_ERR_DISPLAYED} = true;
		}
		return(false);
	}
	return(true);
}

# Purpose: Connect to a Day Planner services server.
# Usage: my $Return = DPS_Connect();
#		The arguments are optional and will be read from %DPServices if not supplied
sub DPS_Connect {
	my $Host = $DPServices{host};
	my $Port = $DPServices{port};
	my $User = $DPServices{user};
	my $Password = $DPServices{pass};
	my $Error;
	# Connect
	setlocale(LC_ALL, 'C');		# Need errors that are not localized for IO_Socket_INET_Errors();
	$DPServices{socket} = IO::Socket::SSL->new(
					PeerAddr => $Host,
					PeerPort => $Port,
					Timeout => 10,
			) or do { $Error = $@; };
			
	setlocale(LC_ALL, '');		# Reset the locale
	# Process errors if any occurred
	if($Error) {
		# If we have already displayed an error to the user this session, don't do it again
		if(defined($DPServices{Offline}) and $DPServices{Offline} == 1) {
			DPS_Error(undef, "Unable to connect to $Host on port $Port: $@");
			return(undef);
		}

		$Error = IO_Socket_INET_Errors($Error);	# Get errors that are easier to process

			# Process network unreachable and bad hostname
		if($Error eq 'OFFLINE' or $Error eq 'BADHOST') {
			$DPServices{Offline} = 1;
			DPS_Error(sprintf($i18n->get('Unable to connect to the Day Planner services server (%s).'), "$Host:$Port",) . " " . $i18n->get("You're probably not connected to the internet"), "Unable to connect to $Host on port $Port: $@ ($Error)");
		}
			# Process connection refused
		elsif($Error eq 'REFUSED') {
			$DPServices{Offline} = 1;
			DPS_Error(sprintf($i18n->get('Unable to connect to the Day Planner services server (%s).'), "$Host:$Port") . ' ' . $i18n->get('The connection was refused by the server. Please verify your Day Planner services settings.') . "\n\n" . $i18n->get('If this problem persists, please contact your service provider'), "Unable to connect to $Host on port $Port: $@ ($Error)");
		} 
			# Process unknown errors
		else {
			DPS_Error(sprintf($i18n->get("Unable to connect to the Day Planner services server (%s)."), "$Host:$Port") . " " . $i18n->get('If this problem persists, please contact your service provider'), "Unable to connect to $Host on port $Port: $@");
		}
		return(undef);
	}

	# The connection didn't fail, so delete $DPServices{Offline} if it exists
	delete($DPServices{Offline});

	# Authentication
	# First verify the API level
	my $APIREPLY = DPS_DataSegment("APILEVEL $DPS_APILevel");
	return(undef) if DPS_ErrorIfNeeded('OK', $APIREPLY, sub { DPS_Disconnect();  DPS_Error($i18n->get_advanced("The Day Planner services server you are connecting to does not support this version of Day Planner (%(version)).", { version =>  $Version}), "API error received from the server (my APILEVEL is $DPS_APILevel).");});
	# Send AUTH
	my $AUTHREPLY = DPS_DataSegment("AUTH $User $Password");
	# If AUTH did not return OK then it failed and we just return undef.
	return(undef) if DPS_ErrorIfNeeded('OK', $AUTHREPLY, sub { DPS_Disconnect(); DPS_Error($i18n->get('The username and/or password is incorrect.'),'Authentication error');});
	DPS_Log("Connected to $Host on port $Port as user $User");
	return('OK');
}

# Purpose: Disconnect from a Day Planner services daemon
# Usage: DPS_Disconnect();
sub DPS_Disconnect {
	my $Socket = $DPServices{socket};
	close($Socket);
	delete($DPServices{socket});
	DPS_Log('Disconnected');
	return(1);
}

# Purpose: Do something when an error has occurred
# Usage: my $Return = DPS_ErrorIfNeeded(EXPECTED_REPLY, RECIEVED_REPLY, CODEREF);
#	The CODEREF will be run if EXPECTED_REPLY does not eq RECIEVED_REPLY
sub DPS_ErrorIfNeeded {
	my ($Expected, $Recieved, $ErrorSub) = @_;
	unless($Expected eq $Recieved) {
		$ErrorSub->($Recieved);
		return(1);
	} else {
		return(0);
	}
}

# Purpose: Send data to a Day Planner services daemon and get the reply
# Usage: my $Reply = DPS_DataSegment(DATA_TO_SEND);
sub DPS_DataSegment {
	my $Socket = $DPServices{socket};
	print $Socket "$_[0]\n";
	my $Data = <$Socket>;
	if(not defined($Data)) {
		$Data = '';
	} else {
		chomp($Data);
	}
	return($Data);
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Data and configuration file functions
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Write the state file 
# Usage: WriteStateFile(DIRECTORY, FILENAME);
sub WriteStateFile {
	# The parameters
	my $Dir = $_[0];
	my $File = $_[1];

	my $NewWinState = 1;

	my %Explenations = (
		MainWin_Maximized => 'If the main window is maximized or not (0=false, 1=true)',
		MainWin_Width => 'The width of the main window',
		MainWin_Height => 'The height of the main window',
		MainWin_X => 'The X-axis position of the main window',
		MainWin_Y => 'The Y-axist position of the main window',
		AutostartOn => 'If the preferences setting for autostarting the daemon on login is on or not',
		AddedAutostart => "If Day Planner has (automatically) set the daemon to start on login\n# (this ONLY sets if it has been automatically added or not.\n# It doesn't say anything about IF it is currently added. AutostartOn sets that.)",
		Holiday_Attempted => 'The last Day Planner version the holiday file was attempted set up (but failed)',
		Holiday_Setup => 'If the holiday file has been properly set up or not (ie. not a dummy file)',
		LastVersion => 'The last Day Planner version used',
		DPS_LastMD5 => 'The MD5 sum of the last data downloaded from the DPS server (base64 encoded)',
		DPS_ParanoidBackups => "Writes a backup of the internal data to $Dir every time DPS is used.\n# This is useful when DPS is being debugged, in order to avoid data loss",
		HEADER => "This file contains internal configuration used by Day Planner\n# In most cases you really don't want to edit this file manually",
	);

	
	if(defined($InternalConfig{MainWin_Maximized}) and $InternalConfig{MainWin_Maximized} =~ /maximized/) {
		$InternalConfig{MainWin_Maximized} = 1;
	} else {
		$InternalConfig{MainWin_Maximized} = 0;
		if($NewWinState and defined($MainWindow)) {
			($InternalConfig{MainWin_Width}, $InternalConfig{MainWin_Height}) = $MainWindow->get_size();
			($InternalConfig{MainWin_X}, $InternalConfig{MainWin_Y}) = $MainWindow->get_position();
		}
	}
	
	# Write the actual file
	WriteConfigFile("$Dir/$File", \%InternalConfig, \%Explenations);
}

# Purpose: Load the state file
# Usage: LoadStateFile(DIRECTORY, FILENAME);
sub LoadStateFile {
	# The parameters
	if(-e "$SaveToDir/state.conf") {
		LoadConfigFile("$SaveToDir/state.conf", \%InternalConfig, undef, 0);
		if(not defined($InternalConfig{LastVersion}) or not $InternalConfig{LastVersion} eq $Version) {
			UpgradeVersion();
		}
	} else {
		$InternalConfig{LastVersion} = $Version;
	}
}

# Purpose: Write the configuration file
# Usage: WriteConfig(DIRECTORY, FILENAME);
sub WriteConfig {
	# The parameters
	my ($Dir,$File) = ($SaveToDir, $ConfigFile);
	# Verify the options first
	unless(defined($UserConfig{Events_NotifyPre}) and length($UserConfig{Events_NotifyPre})) {
		$UserConfig{Events_NotifyPre} = '30min';
	}
	unless(defined($UserConfig{Events_DayNotify}) and length($UserConfig{Events_DayNotify})) {
		$UserConfig{Events_DayNotify} = 0;
	}
	if(not defined($UserConfig{DPS_enable}) or not length($UserConfig{DPS_enable})) {
		$UserConfig{DPS_enable} = 0;
	}
	if(defined($UserConfig{DPS_pass}) and length($UserConfig{DPS_pass})) {
		$UserConfig{DPS_pass} = encode_base64(encode_base64($UserConfig{DPS_pass}));
		chomp($UserConfig{DPS_pass});
	}

	my %Explanations = (
		Events_NotifyPre => "If Day Planner should notify about an event ahead of time.\n#  0 = Don't notify\n# Other valid values: 10min, 20min, 30min, 45min, 1hr, 2hrs, 4hrs, 6hrs",
		Events_DayNotify => "If Day Planner should notify about an event one day before it occurs.\n#  0 - Don't notify one day in advance\n#  1 - Do notify one day in advance",
		DPS_host => 'The DPS host to connect to',
		DPS_pass => 'The password',
		DPS_port => 'The port to connect to on the DPS server',
		DPS_user => 'The username',
		DPS_enable => 'If DPS (Day Planner services) is enabled or not (1/0)',
		HTTP_Calendars => 'HTTP Calendar subscriptions for Day Planner 0.9 and later',
		HEADER => "Day Planner $Version configuration file",
	);
	
	# Write the actual file
	WriteConfigFile("$Dir/$File", \%UserConfig, \%Explanations);

	# Tell the daemon to reload the config file
	if($DaemonInitialized) {
		Daemon_SendData('RELOAD_CONFIG');
	}
	# Reset DPS_pass
	if(defined($UserConfig{DPS_pass}) and length($UserConfig{DPS_pass})) {
		$UserConfig{DPS_pass} = decode_base64(decode_base64($UserConfig{DPS_pass}));
	}
	# Enforce perms
	chmod(oct(600),"$Dir/$File");
}

# Purpose: Load the configuration file
# Usage: LoadConfig();
sub LoadConfig {
	# The parameters
	my ($Dir,$File) = ($SaveToDir, $ConfigFile);
	# If it doesn't exist then we just let WriteConfig handle it
	unless (-e "$Dir/$File") {
		WriteConfig($Dir, $File);
		return(1);
	}
	
	my %OptionRegexHash = (
			Events_NotifyPre => '^(\d+(min|hrs?){1}|0){1}$',
			Events_DayNotify => '^\d+$',
			DPS_enable => '^(1|0)$',
			DPS_port => '^\d+$',
			DPS_user => '^.+$',
			DPS_host => '^.+$',
			DPS_pass => '^.+$',
			HTTP_Calendars => '^.*$',
		);

	LoadConfigFile("$Dir/$File", \%UserConfig, \%OptionRegexHash,1);
	if(defined($UserConfig{DPS_pass}) and length($UserConfig{DPS_pass})) {
		$UserConfig{DPS_pass} = decode_base64(decode_base64($UserConfig{DPS_pass}));
	}
	return(1);
}

# Purpose: Create the directory in $SaveToDir if it doesn't exist and display a error if it fails
# Usage: CreateSaveDir();
sub CreateSaveDir {
	if(not -e $SaveToDir)
	{
		runtime_use('File::Path');
		File::Path::mkpath($SaveToDir) or do {
				DPError($i18n->get_advanced("Unable to create the directory %(directory): %(error)\nManually create this directory before closing this dialog.", { directory => $SaveToDir, error => $!}));
				unless(-d $SaveToDir) {
					die("$SaveToDir does not exist, I was unable to create it and the user didn't create it\n");
				}
		};
		chmod(oct(700),$SaveToDir);
	}
}

# Purpose: Perform certain commands on first startup
# Usage: FirstStartup(GUI?);
sub FirstStartup {
	# First create the savedir
	CreateSaveDir();
	# Set LastVersion
	$InternalConfig{LastVersion} = $Version;
	if(not $_[0]) {
		# Attempt to import data
		return(ImportDataFromProgram(1));
	}
	return(0);
}	

# Purpose: Save the main data files
# Usage: SaveMainData();
sub SaveMainData {
	$iCalendar->write();	# TODO: Check return value
	# This to avoid unneccesary overhead in the daemon reloading files needlessly
	Daemon_SendData('RELOAD_DATA');
	return(true);
}

# Purpose: Save the data file and redraw the needed windows
# Usage: UpdatedData(DONT_SAVE?, DONT_REDRAW_EVENTLIST);
sub UpdatedData {
	my ($DontSave,$DontRedrawEventList) = @_;
	if($MainWindow) {
		if(not $DontRedrawEventList) {
			# Redraw the event list
			PopulateEventList();
		}
		# Repopulate the upcoming events
		PopulateUpcomingEvents();
		# Redraw the calendar
		CalendarChange();
	}
	# Lastly, save the main data if needed.
	# We do this last because that speeds up redrawing the calendar
	if(not $DontSave) {
		# Save the data
		SaveMainData();
	}
}

# Purpose: Load the calendar contents
# Usage: LoadCalendar();
sub LoadCalendar {
	my $IsFirstStartup = shift;
	# Create the manager
	$iCalendar = DP::iCalendar::Manager->new();
	my $MainCalendar;
	if(-e "$SaveToDir/$ICSFile") {
		# Load an already existing calendar
		$MainCalendar = DP::iCalendar->new("$SaveToDir/$ICSFile");
	} else {
		# Create a new calendar
		$MainCalendar = DP::iCalendar->newfile("$SaveToDir/$ICSFile");
	}
	# Relax the file permissions if we're told to
	if (defined($ENV{DP_NO_STRICT_PERMS}) and $ENV{DP_NO_STRICT_PERMS} eq '1')
	{
		$MainCalendar->set_file_perms(oct(644));
	}
	# Manage it
	$iCalendar->add_object($MainCalendar,true);
	# Make sure we have a holidays file before trying to load one
	if (not -e "$SaveToDir/holidays") 
	{
		HolidaySetup();
	}
	# If we still don't have one then skip this step
	if (-e "$SaveToDir/holidays")
	{
		# Load the holiday file
		$HolidayParser = Date::HolidayParser->new("$SaveToDir/holidays");
		# Enable Date::HolidayParsers DP::iCalendar-compatible interface, replacing
		# the standard interface.
		$HolidayParser->enable_ical_interface();
		# Add the holiday parser to the manager
		$iCalendar->add_object($HolidayParser,false);
	}
	# Set the ICS prodid
	$iCalendar->set_prodid("-//day-planner.org//NONSGML Day Planner $Version//EN");
	return(1);
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# General helper functions
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

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

# Purpose: Get OS/distro version information
# Usage: print "OS: ",GetDistVer(),"\n";
sub GetDistVer {
	# Try LSB first
	my %LSB;
	if (-e '/etc/lsb-release')
	{
		LoadConfigFile('/etc/lsb-release',\%LSB);
		if(defined($LSB{DISTRIB_ID}) and $LSB{DISTRIB_ID} =~ /\S/ and defined($LSB{DISTRIB_RELEASE}) and $LSB{DISTRIB_RELEASE} =~ /\S/)
		{
			my $ret = '/etc/lsb-release: '.$LSB{DISTRIB_ID}.' '.$LSB{DISTRIB_RELEASE};
			if(defined($LSB{DISTRIB_CODENAME}))
			{
				$ret .= ' ('.$LSB{DISTRIB_CODENAME}.')';
			}
			return($ret);
		}
	}
	# GNU/Linux and BSD
	foreach(qw/mandriva mandrakelinux mandrake fedora redhat red-hat ubuntu debian gentoo suse distro dist slackware freebsd openbsd netbsd dragonflybsd NULL/) {
		if (-e "/etc/$_-release" or -e "/etc/$_-version" or -e "/etc/${_}_version" or $_ eq "NULL") {
			my ($DistVer, $File, $VERSION_FILE);
			if(-e "/etc/$_-release") {
				$File = "$_-release";
				open($VERSION_FILE, '<', "/etc/$_-release");
				$DistVer = <$VERSION_FILE>;
			} elsif (-e "/etc/$_-version") {
				$File = "$_-version";
				open($VERSION_FILE, '<', "/etc/$_-release");
				$DistVer = <$VERSION_FILE>;
			} elsif (-e "/etc/${_}_version") {
				$File = "${_}_version";
				open($VERSION_FILE, '<', "/etc/${_}_version");
				$DistVer = <$VERSION_FILE>;
			} elsif ($_ eq 'NULL') {
				last unless -e '/etc/version';
				$File = 'version';
				open($VERSION_FILE, '<', '/etc/version');
				$DistVer = <$VERSION_FILE>;
			}
			close($VERSION_FILE);
			chomp($DistVer);
			return("/etc/$File: $DistVer");
		}
	}
	# Didn't find anything yet. Get uname info
	my ($sysname, $nodename, $release, $version, $machine) = POSIX::uname();
	if ($sysname =~ /darwin/i) {
		my $DarwinName;
		my $DarwinOSVer;
		# Darwin kernel, try to get OS X info.
		if(InPath('sw_vers')) {
			if(eval('use IPC::Open2;1')) {
				if(open2(my $SW_VERS, my $NULL_IN, 'sw_vers')) {
					while(<$SW_VERS>) {
						chomp;
						if (s/^ProductName:\s+//gi) {
							$DarwinName = $_;
						} elsif(s/^ProductVersion:\s+//) {
							$DarwinOSVer = $_;
						}
					}
					close($SW_VERS);
				}
			}
		}
		if(defined($DarwinOSVer) and defined($DarwinName)) {
			return("$DarwinName $DarwinOSVer ($machine)");
		}
	}
	# Some distros set a LSB DISTRIB_ID but no version, try DISTRIB_ID
	# along with the kernel info.
	if ($LSB{DISTRIB_ID})
	{
		return($LSB{DISTRIB_ID}."/Unknown ($sysname $release $version $machine)");
	}
	return("Unknown ($sysname $release $version $machine)");
}

# Purpose: Launch a web browser with the supplied URL
# Usage: LaunchWebBrowser(URL);
sub LaunchWebBrowser {
	my $URL = shift;
	# Check if URL is a ref. If it is that means we're being used in a gtk2 callback
	# and the first arg is the object we're called from, so shift again to the second
	# arg we recieved which is the real url.
	if(ref($URL)) {
		$URL = shift;
	}
	my $Browser;
	# First check for the BROWSER env var
	if(defined($ENV{BROWSER}) and length($ENV{BROWSER})) {
		# Allow it to be a :-seperated variable - this doesn't slow us down
		# and is future-proof(tm)
		foreach my $Part (split(/:/,$ENV{BROWSER}))
		{
			if(InPath($Part) or -x $Part) {
				$Browser = $Part;
			}
		}
	}
	# Then check for various known file launchers and web browsers
	if(not $Browser) {
		foreach(qw/xdg-open gnome-open exo-open mozffremote mozilla-firefox firefox iceweasel epiphany galeon midori mozilla seamonkey konqueror dillo opera www-browser/) {
			if(InPath($_)) {
				$Browser = $_;
				last;
			}
		}
	}
	# Then launch if found, or output an error if not found
	if($Browser) {
		my $PID = fork();
		if(not $PID) {
			exec($Browser,$URL);
		}
	} else {
		# This should very rarely happen
		DPIntWarn("Failed to detect any browser to launch for the URL $URL");
	}
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Core helper functions
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Load a module during runtime or display an error if we can't
# Usage: runtime_use('MODULENAME',SILENT?);
# 	If not in silent mode it will output an error about a missing core module
# 	and explain that the installation is corrupt.
sub runtime_use
{
	my $module = shift;
	# Return if it is present in the hash, regardless of true/false value.
	return($RuntimeModules{$module}) if defined($RuntimeModules{$module});
	if(eval("use $module; 1")) {
		$RuntimeModules{$module} = true;
		return(true);
	} else {
		$RuntimeModules{$module} = false;
		my $silent = shift;
		if(not $silent) {
			DPIntWarn("FATAL: Module missing: $module");
			DPError($i18n->get_advanced("A fatal error has occurred. Your installation of Day Planner is missing some files. This makes certain functions in Day Planner unusable. Please re-install Day Planner. See %(website) for more information on how to download and re-install Day Planner.\nDay Planner will continue to run but it is likely to crash.\n\n(The missing file was: %(module))", { website => 'http://www.day-planner.org/', module => $module }));
		}
		return(false);
	}
}

# Purpose: Display an error about an already running Day Planner session
# Usage: AlreadyRunningDP(IPC_CLIENT);
sub AlreadyRunningDP {
	my $ClientConnection = shift;
	if($Gtk2Init) {
		my $ErrorDialog = Gtk2::MessageDialog->new (undef,
						'destroy-with-parent',
						'error',
						'none',
						 $i18n->get("Another instance of Day Planner appears to be running. Please quit the currently running instance of Day Planner before continuing\n\nYou may force the other instance to quit if you wish."));
		$ErrorDialog->set_title($i18n->get('Day Planner'));

		$ErrorDialog->add_buttons(
			# TRANSLATORS: As in force another running Day Planner instance to quit
			$i18n->get('_Force') => 'reject',
			'gtk-ok' => 'accept',
		);
	
		$ErrorDialog->set_default_response('accept');
		if($ErrorDialog->run eq 'accept') {
			$ErrorDialog->destroy();
			$ClientConnection->destroy();
			exit(0)
		} else {
			$ErrorDialog->destroy;
			$ClientConnection->client_send('QUIT');
			$ClientConnection->destroy();
			# Sleep for one second to allow the other instance to quit
			sleep(1);
			# Unlink the IPC socket
			unlink("$SaveToDir/ipcsocket");
			$iCalendar->reload();
			return(true);
		}
	} else {
		$ClientConnection->destroy();
		die("Another instance of Day Planner appears to be running, unable to continue\n");
	}
}

# Purpose: Handle IPC talking
# Usage: IPC_Handler();
sub IPC_Handler {
	my $Message = shift;
	chomp($Message);
	if($Message =~ s/^IMPORT_DATA\s+(.+)$/$1/) {
		$iCalendar->addfile($Message);
		print "Imported $Message\n";
		UpdatedData();
	} elsif ($Message =~ /^QUIT/) {
		# This is the same as ctrl+c
		DP_SigHandler('IPC_QUIT_REQUEST');
	} else {
		return('UNKNOWN_CMD');
	}
}

# Purpose: Get the SUMMARY string for an UID
# Usage: my $Summary = GetSummaryString(UID, CURRENT?, YEAR?, MONTH?);
#  The returned summary string is properly formatted (ie. it will return
#  a properly localized birthday string if needed).
#
# 	If CURRENT is true GetSummaryString() will base its birthday information
# 	on the current date/time instead of the selected date/time
# 	If YEAR/MONTH is true and CURRENT is false then that will be used in place
# 	of the selection from $CalendarWidget
sub GetSummaryString {
	my $UID = shift;
	my $CURRENT = shift;
	my ($AltYear, $AltMonth) = @_;
	my $UID_Obj = $iCalendar->get_info($UID);
	if(defined($UID_Obj->{'X-DP-BIRTHDAY'}) and $UID_Obj->{'X-DP-BIRTHDAY'} eq 'TRUE') {
		if($UID_Obj->{'X-DP-BIRTHDAYNAME'}) {
			if(defined($UID_Obj->{'X-DP-BORNATDTSTART'}) and $UID_Obj->{'X-DP-BORNATDTSTART'} eq 'TRUE') {
				# Calculate birthday here
				my ($BYear,$BMonth,$BDay) = iCal_ParseDateTime($UID_Obj->{'DTSTART'});
				my ($SelYear, $SelMonth, $SelDay);
				# If CalendarWidget is false and no arguments has been supplied, warn and set current
				if(not $CURRENT and not $CalendarWidget and not ($AltYear and $AltMonth)) {
					$CURRENT = 1;
					DPIntWarn("Bug: GetSummaryString() called without CURRENT and without Year/Month. Assuming CURRENT");
				}
				if(not $CURRENT) {
					if($AltYear and $AltMonth) {
						$SelYear = $AltYear;
						$SelMonth = $AltMonth;
					} else {
						($SelYear, $SelMonth, $SelDay) = $CalendarWidget->get_date();$SelMonth++;
					}
				} else {
					# Get the current time
					my ($currsec,$currmin,$currhour,$currmday,$currmonth,$curryear,$currwday,$curryday,$currisdst) = GetDate();
					$SelYear = $curryear;
					$SelMonth = $currmonth;
				}

				my $YearsOld = $SelYear - $BYear;

				# Handle the December/January change
				if($SelMonth == 12 && $BMonth == 1) {
					$YearsOld++;
				}

				if($YearsOld == 0) {
					return($i18n->get_advanced('%(name) was born', { name => $UID_Obj->{'X-DP-BIRTHDAYNAME'}}));
				} elsif ($YearsOld < 0) {
					return('');
				} else {
					return($i18n->get_advanced("%(name)'s birthday (%(year) years old)", { name => $UID_Obj->{'X-DP-BIRTHDAYNAME'}, year => $YearsOld}));
				}

			} else {
				return($i18n->get_advanced("%(name)'s birthday",{ name => $UID_Obj->{'X-DP-BIRTHDAYNAME'}}));
			}
		} else {
			DPIntWarn("UID $UID is set to be a birthday but is missing X-DP-BIRTHDAYNAME, using SUMMARY string");
		}
	}
	if(not $UID_Obj->{SUMMARY}) {
		DPIntWarn("No SUMMARY found for the UID $UID");
		# TRANSLATORS: This string is used when there is some issue getting the description
		# 	of an event. It is rarely used but kept just in case.
		return($i18n->get('Unknown'));
	}
	return($UID_Obj->{SUMMARY});
}

# Purpose: Upgrade from one version to another
# Usage: UpgradeVersion();
sub UpgradeVersion {
	my $ProgressWin = DPCreateProgressWin($i18n->get('Upgrading'), '', 1);
	$InternalConfig{LastVersion} = $Version;
	my $Total = 9;
	# Upgrade the holiday file if needed
	if(-e $HolidayFile) {
		ProgressMade(1, $Total, $ProgressWin) if($Gtk2Init);
		if(-e "$FindBin::RealBin/holiday/dayplanner_upgrade") {
			runtime_use('Digest::MD5 qw(md5_base64 md5_hex)');
			my %UpgradeInfo;
			LoadConfigFile("$FindBin::RealBin/holiday/dayplanner_upgrade", \%UpgradeInfo, undef, 0);
			ProgressMade(2, $Total, $ProgressWin) if($Gtk2Init);
			open(my $ReadHoliday, '<', $HolidayFile);
			my $HolidayMd5 = md5_hex(<$ReadHoliday>);
			close($ReadHoliday);
			if($UpgradeInfo{$HolidayMd5}) {
				if (-e "$FindBin::RealBin/holiday/$UpgradeInfo{$HolidayMd5}") {
					runtime_use('File::Copy');
					# Copy the new file in place
					unlink($HolidayFile);
					copy("$FindBin::RealBin/holiday/$UpgradeInfo{$HolidayMd5}", $HolidayFile);
					DPIntInfo("Upgraded $HolidayFile");
					ProgressMade(3, $Total, $ProgressWin) if($Gtk2Init);
				}
			}
		}
	}
	# Upgrade the old data files if needed
	if(not -e "$SaveToDir/$ICSFile") {
		# Normal events (events.dpd)
		if(-e "$SaveToDir/events.dpd") {
			my %CalendarContents = do("$SaveToDir/events.dpd");
			foreach my $Year(keys(%CalendarContents)) {
				foreach my $Month (keys(%{$CalendarContents{$Year}})) {
					foreach my $Day (keys(%{$CalendarContents{$Year}{$Month}})) {
						foreach my $Time (keys(%{$CalendarContents{$Year}{$Month}{$Day}})) {
							# Create the data hash
							my %iCalData;
							$iCalData{SUMMARY} = $CalendarContents{$Year}{$Month}{$Day}{$Time}{summary};
							if(defined($CalendarContents{$Year}{$Month}{$Day}{$Time}{fulltext}) and length($CalendarContents{$Year}{$Month}{$Day}{$Time}{fulltext})) {
								$iCalData{DESCRIPTION} = $CalendarContents{$Year}{$Month}{$Day}{$Time}{fulltext};
							}
							$iCalData{DTSTART} = iCal_GenDateTime($Year, $Month, $Day, $Time);
							$iCalData{DTEND} = $iCalData{DTSTART};
							$iCalendar->add(%iCalData);
						}
					}
				}
			}
		}
		ProgressMade(4, $Total, $ProgressWin) if($Gtk2Init);
		# Birthday events (birthdays.dpd)
		if(-e "$SaveToDir/birthdays.dpd") {
			my %BirthdayContents = do("$SaveToDir/birthdays.dpd");
			foreach my $Month (keys(%BirthdayContents)) {
				foreach my $Day (keys(%{$BirthdayContents{$Month}})) {
					foreach my $Name (keys(%{$BirthdayContents{$Month}{$Day}})) {
						# Create the data hash
						my %iCalData;
						$iCalData{DTSTART} = iCal_GenDateTime(1970, $Month, $Day, '00:00');
						$iCalData{DTEND} = $iCalData{DTSTART};
						$iCalData{'X-DP-BIRTHDAY'} = 'TRUE';
						$iCalData{'X-DP-BIRTHDAYNAME'} = $Name;
						$iCalData{RRULE} = 'FREQ=YEARLY';
						$iCalData{SUMMARY} = $i18n->get_advanced("%(name)'s birthday",{ name => $Name});
						$iCalendar->add(%iCalData);
					}
				}
			}
		}
		ProgressMade(5, $Total, $ProgressWin) if($Gtk2Init);
		# Write the iCalendar file.
		$iCalendar->write();
		ProgressMade(6, $Total, $ProgressWin) if($Gtk2Init);
	}
	foreach(qw/special_events.dpd events.dpd birthdays.dpd/) {
		unlink("$SaveToDir/$_") if -e "$SaveToDir/$_";
	}
	ProgressMade(7, $Total, $ProgressWin) if ($Gtk2Init);
	# Upgrade old daemon (ie. kill it) if needed
	if (-e "$SaveToDir/dayplannerd")
	{
		my $Old_Daemon = DP::GeneralHelpers::IPC->new_client("$SaveToDir/dayplannerd");
		if ($Old_Daemon)
		{
			# Authenticate as a commander, this has no access control restrictions.
			$Old_Daemon->client_send("$$ HI commander");
			$Old_Daemon->client_send("$$ SHUTDOWN");
			$Old_Daemon->destroy();
		}
		# Ensure that the socket is gone
		unlink("$SaveToDir/dayplannerd") if -e "$SaveToDir/dayplannerd";
	}
	ProgressMade(8, $Total, $ProgressWin) if($Gtk2Init);
	# Remove and re-add autostart if it is enabled, so that
	# the autostart is correct (daemon path might have changed)
	if ($InternalConfig{AutostartOn} eq '1')
	{
		RemoveAutostart();
		DP_AddAutostart();
	}
	ProgressMade(9, $Total, $ProgressWin) if($Gtk2Init);
	$ProgressWin->{Window}->destroy();
}

# Purpose: Handle various signals gracefully
# Usage: $SIG{SIGNAL} = \&DP_SigHandler;
sub DP_SigHandler {
	$| = 1;
	print "SIG$_[0] recieved.\nSaving data...";
	if($iCalendar) {
		if(SaveMainData() eq 'SAVE_FAILED') {
			print 'FAILED';
		} else {
			print 'done';
		}
	} else {
		print 'not needed';
	}
	print "\nWriting state file...";
	if($SaveToDir and scalar keys %InternalConfig) {
		WriteStateFile($SaveToDir, "state.conf");
		print "done\n";
	} else {
		print "not needed\n";
	}
	print "Closing the daemon connection...";
	CloseIPC();
	print "done\nExiting the gtk2 main loop...";
	if($Gtk2Init) {
		Gtk2->main_quit;
		print "done\nExiting\n";
	} else {
		print "not running\nExiting\n";
	}
	exit(0);
}

# Purpose: Make sure we have a .holiday set up
# Usage: HolidaySetup();
sub HolidaySetup {
	# If Holiday_Setup is true and the file exists then Day Planner has properly set up
	# a holiday-file.
	#
	# If Holiday_Attempted is defined and equal to $Version then we've attempted and failed
	# to set up a holiday file in this version of Day Planner so we'll skip trying again.
	if($InternalConfig{Holiday_Setup} and -e $HolidayFile) {
		return(1);
	} elsif(defined($InternalConfig{Holiday_Attempted}) and $InternalConfig{Holiday_Attempted} eq $Version) {
		return(1);
	}

	# Yay, the user already has a .holiday file. Use this one
	if(-e "$ENV{HOME}/.holiday") {
		symlink("$ENV{HOME}/.holiday",$HolidayFile);
		$InternalConfig{Holiday_Setup} = 1; delete($InternalConfig{Holiday_Attempted}) if defined($InternalConfig{Holiday_Attempted});
		return(1);
	}

	# Hash of LC_ADDRESS code => holiday file
	# Loaded from dayplanner_detection
	my %HolidayFiles;
	if(-e "$FindBin::RealBin/holiday/dayplanner_detection") {
		LoadConfigFile("$FindBin::RealBin/holiday/dayplanner_detection", \%HolidayFiles, undef, 0)
			or DPIntWarn('Unable to load holiday detection definitions!');
	}
	
	my $LocationDetect;

	# First try to fetch from the environment
	foreach my $var(qw(LC_ADDRESS LC_TELEPHONE LC_IDENTIFICATION LC_MESSAGES LC_ALL LANGUAGE LANG LC_MONETARY LC_NUMERIC LC_TIME LC_COLLATE LC_CTYPE))
	{
		if(defined($ENV{$var}))
		{
			$LocationDetect = $ENV{$var};
			last;
		}
	}
	# If that didn't work, try to get it from setlocale()
	if(not $LocationDetect)
	{
		foreach my $var([LC_ALL, LC_COLLATE, LC_CTYPE, LC_MONETARY, LC_NUMERIC, LC_TIME])
		{
			$LocationDetect = setlocale($var);
			# If it is defined, not of zero length and not set to C then we can use it, if not
			# then discard this value and continue
			if(defined($LocationDetect) and length($LocationDetect) and not $LocationDetect eq 'C')
			{
				last;
			}
			else
			{
				$LocationDetect = undef;
			}
		}
	}
	# If THAT didn't work either, fall back to guessing environment variables
	if(not $LocationDetect)
	{
		my $BaseWarning = 'Neither of the environment variables LC_ADDRESS, LC_TELEPHONE, LC_IDENTIFICATION, LC_MESSAGES, LC_ALL nor LANGUAGE was set. ';
		# Fall back to automatic detection
		foreach my $key (keys(%ENV))
		{
			if ($key =~ /^(LANG|LC_)/)
			{
				my $cont = $ENV{$key};
				if ($cont =~ /^\w\w_\w\w(\..*)/)
				{
					DPIntWarn($BaseWarning . "Unable to reliably detect .holiday file. Fell back to alternate method, found $key in environment ($key=$cont) - using that for detection");
				}
				else
				{
					DPIntWarn($BaseWarning.'Unable to detect .holiday file');
				}
			}
		}
	}

	my $CopyFile;
	
	if(defined($LocationDetect)) {
		# Let's try to find the LocationDetect value in the %HolidayFiles hash
		# We sort it so that we test the longest entries before testing the short ones
		foreach my $Key (sort {length($b) <=> length($a)} keys(%HolidayFiles)) {
			if($LocationDetect =~ /^$Key/) {
				$CopyFile = $HolidayFiles{$Key};
				last;
			}
		}
		if(defined($CopyFile)) {
			if(-e "$FindBin::RealBin/holiday/holiday_$CopyFile") {
				runtime_use('File::Copy');
				copy("$FindBin::RealBin/holiday/holiday_$CopyFile", $HolidayFile);
				$InternalConfig{Holiday_Setup} = 1; delete($InternalConfig{Holiday_Attempted}) if defined($InternalConfig{Holiday_Attempted});
				return(1);
			} else {
				DPIntWarn("The .holiday file detected for you (holiday_$CopyFile at $FindBin::RealBin/holiday/holiday_$CopyFile) did not exist.");
			}
		} else {
			DPIntWarn("Couldn't detect a .holiday file for $LocationDetect. Maybe you would like to write one?");
		}
	}
	open(my $DUMMY_FILE, '>', "$HolidayFile");
	if($LocationDetect) {
		print $DUMMY_FILE ": This is a dummy .holiday file for Day Planner. It couldn't detect a proper\n";
		print $DUMMY_FILE ": one suitable for your location (which at the time was detected to be $LocationDetect).\n";
		print $DUMMY_FILE ": You may want to write one yourself (see the files contained in the holiday/ directory\n";
		print $DUMMY_FILE ":  of the Day Planner distribution for examples of the syntax).\n";
		print $DUMMY_FILE "\n: HOW TO REPLACE THE DUMMY FILE WITH A PROPER ONE:\n: Remove this file, copy the proper file to ~/.holiday and re-run Day Planner";
	} else {
		print $DUMMY_FILE ": This is a dummy .holiday file for Day Planner. You didn't have any\n";
		print $DUMMY_FILE ": environment variables set so Day Planner couldn't autodetect one.\n";
		print $DUMMY_FILE "\n: HOW TO REPLACE THE DUMMY FILE WITH A PROPER ONE:\n: Remove this file. Remove the entry Holiday_Attempted\n: in state.conf and re-run Day Planner.\n";
		print $DUMMY_FILE ": Make sure one of the LC_* or LANG* environment variables are set when running Day Planner";
	}
	print $DUMMY_FILE "\n\n: Day Planner will automatically re-try to detect a holiday file after it has been updated.";
	close($DUMMY_FILE);
	$InternalConfig{Holiday_Setup} = 0;
	$InternalConfig{Holiday_Attempted} = $Version;
	return(0);
}

# Purpose: Remove the daemon from autostart for the various DMs/WMs
# Usage: RemoveAutostart();
sub RemoveAutostart {
	# KDE, XDG - easy
	foreach("$ENV{HOME}/.kde/Autostart/dayplanner_auto.sh", "$ENV{HOME}/.config/autostart/dayplanner_auto.desktop",) {
		unlink($_) if -e $_;
	}

	# Fluxbox
	if(-w "$ENV{HOME}/.fluxbox/startup") {
		my @FluxboxStartup;
		open(my $OLDFLUX, '<', "$ENV{HOME}/.fluxbox/startup");
		push(@FluxboxStartup, $_) while(<$OLDFLUX>);
		close($OLDFLUX);
		open(my $FLUXSTART, '>', "$ENV{HOME}/.fluxbox/startup");
		foreach(@FluxboxStartup) {
			unless(/$DaemonName/) {
				chomp;
				print $FLUXSTART "$_\n";
			}
		}
		close($FLUXSTART);
	}
	# GNOME 2 versions 2.12 and older.
	#  This solution is very very hacky and unreliable, but it will have to do.
	if(-w "$ENV{HOME}/.gnome2/session-manual") {
		my @Gnome2Session;
		my $GNOME2SESS;
		open($GNOME2SESS, '<', "$ENV{HOME}/.gnome2/session-manual");
		push(@Gnome2Session, $_) while(<$GNOME2SESS>);
		close($GNOME2SESS);
		my ($Last, $Has_DP, $DP_ID, $Nullout);
		# Parse
		foreach(@Gnome2Session) {
			next unless /\=/;
			next unless /^\d,/;
			my $ID = $_;
			$ID =~ s/^(\d+).*/$1/;
			if(/$DaemonName/) {
				$DP_ID = $ID;
				$Has_DP = 1;
			}
			$Last = $ID;
			# Make sure we're not starting on yet another session.
			if($ID == 0) {
				# GAH, another session decleration. This is too much for us to parse.
				# Use the alternate nullout method (continue attempting to parse if Has_DP is
				# false
				$Nullout = 1;
				last if $Has_DP;
			}
		}
		
		# We've got three ways to do this, one more hacky than the next - but
		# as gnome2 (2.12 and older) doesn't have any good way of dealing with
		# this we'll have to do this.
		if($Has_DP) {
			if($Last == 0) {
				# Wee, LAST is 0, just 0 out the file
				open($GNOME2SESS, '>', "$ENV{HOME}/.gnome2/session-manual");
				close($GNOME2SESS);
			} elsif($Nullout) {
				DPIntWarn("Unable to properly remove the GNOME2 Day Planner entry from $ENV{HOME}/.gnome2/session-manual. A workaround to disable the daemon startup has been applied but you may want to use the GNOME2 session manager to remove it completely.");
				foreach(@Gnome2Session) {
					s/=.*$DaemonName.*/perl -e '# Removed by Day Planner'/;
				}
				open($GNOME2SESS, '>', "$ENV{HOME}/.gnome2/session-manual");
				foreach(@Gnome2Session) {
					print $GNOME2SESS $_;
				}
				close($GNOME2SESS);
			} else {
				my $NumClients = $Last + 1;
				# Write out changes
				open($GNOME2SESS, '>', "$ENV{HOME}/.gnome2/session-manual");
				foreach(@Gnome2Session) {
					chomp;
					s/^num_clients=\d+/num_clients=$Last/;
					s/^$Last,/$DP_ID,/;
					next if /^$DP_ID,/;
					print $GNOME2SESS "$_\n";
				}
				close($GNOME2SESS);
			}
		}
	}
	return(1);
}

# Purpose: Add the daemon to autostart for the various DMs/WMs
# Usage: AddAutostart(OLD_GNOME?);
#	OLD_GNOME is either 1 or 0, defines if the GNOME version used is
#	older than 2.13 or not
sub AddAutostart {
	my $TryGnome = $_[0];
	my $DaemonExec;
	my $RETURN = 'okay';
	foreach(split(/:/, sprintf('%s:%s', $FindBin::RealBin, $ENV{PATH} ))) {
		if (-x "$_/$DaemonName") {
			$DaemonExec = "$_/$DaemonName";
			last;
		}
	}
	die('Unable to detect the daemon') unless(defined($DaemonExec));

	# KDE - this is easy
	if(-d "$ENV{HOME}/.kde/") {
		mkdir("$ENV{HOME}/.kde/Autostart/") unless -e "$ENV{HOME}/.kde/Autostart/";
		open(my $KDESCRIPT, '>', "$ENV{HOME}/.kde/Autostart/dayplanner_auto.sh") or warn("Unable to open $ENV{HOME}/.kde/Autostart/dayplanner_auto.sh for writing: $!");
		if($KDESCRIPT) {
			print $KDESCRIPT "#!/bin/sh\n# Autogenerated startup script written by Day Planner\n";
			print $KDESCRIPT "$DaemonExec\n";
			close($KDESCRIPT);
			chmod(oct(700), $ENV{HOME}.'/.kde/Autostart/dayplanner_auto.sh');
		}
	}
	# Freedesktop spec - this is easy too
	if(-d "$ENV{HOME}/.config/") {
		mkdir("$ENV{HOME}/.config/autostart/") unless -e "$ENV{HOME}/.config/autostart";
		open(my $XDGSTART, '>', "$ENV{HOME}/.config/autostart/dayplanner_auto.desktop");
		print $XDGSTART "# Autogenerated startup desktop file written by Day Planner\n";
		print $XDGSTART "[Desktop Entry]\n";
		print $XDGSTART "Version=1.0\n";
		print $XDGSTART "Encoding=UTF-8\n";
		print $XDGSTART "Type=Application\n";
		print $XDGSTART "Name=Day Planner Reminder\n";
		print $XDGSTART "Comment=Ensures you get notified about events\n";
		print $XDGSTART "Exec=$DaemonExec\n";
		print $XDGSTART "StartupNotify=false\n";
		print $XDGSTART "StartupWMClass=false\n";
		print $XDGSTART "Terminal=false\n";
		# To ensure it is autostarted, we add this. It isn't strictly required, but doesn't hurt
		print $XDGSTART "X-GNOME-Autostart-enabled=true\n";
		# This is hacky, but we'll try anyway
		my $Locale = setlocale(LC_ALL);
		$Locale =~ s/^(\w\w)(_\w\w)?.*/$1$2/;
		if(not $Locale =~ /^en/ and length($Locale))
		{
			my $LocalizedName = $i18n->get('Day Planner Reminder');
			$LocalizedName = undef
				if ($LocalizedName eq 'Day Planner Reminder');
			# TRANSLATORS: This is the comment used in the autostart .desktop file about the Day Planner Reminder
			my $LocalizedComment = $i18n->get('Ensures you get notified about events');
			$LocalizedComment = undef
				if ($LocalizedComment eq 'Ensures you get notified about events');
			# We print twice to ensure the locale is correct
			print $XDGSTART "Name[$Locale]=$LocalizedName\n" if $LocalizedName;
			print $XDGSTART "Comment[$Locale]=$LocalizedComment\n" if $LocalizedComment;
			$Locale =~ s/^(\w\w).*/$1/;
			print $XDGSTART "Name[$Locale]=$LocalizedName\n" if $LocalizedName;
			print $XDGSTART "Comment[$Locale]=$LocalizedComment\n" if $LocalizedComment;
		}
		close($XDGSTART);
	}
	# GNOME - this is harder. No real common spec or easy way to do it. 2.14 or so
	#  supports the freedesktop spec, older however - does not.
	if(-d "$ENV{HOME}/.gnome2/" and $TryGnome) {
		# If it doesn't exist then we just write it
		unless(-e "$ENV{HOME}/.gnome2/session-manual") {
			open(my $GNOMESESS, '>', "$ENV{HOME}/.gnome2/session-manual");
			print $GNOMESESS "[Default]\nnum_clients=1\n0,RestartStyleHint=3\n0,Priority=50\n0,RestartCommand=$DaemonExec\n0,Program=$DaemonExec";
			close($GNOMESESS);
		} else {
			$RETURN = 'gnome-fail';
			my @G2H;
			open(my $GNOMESESS, '<', "$ENV{HOME}/.gnome2/session-manual");
			while(<$GNOMESESS>) {
				if(s/perl -e '# Removed by Day Planner'/$DaemonExec/i) {
					$RETURN = 'okay';
				}
				push(@G2H, $_);
			}
			close($GNOMESESS);
			if($RETURN eq 'okay') {
				open($GNOMESESS, '>', "$ENV{HOME}/.gnome2/session-manual");
				print $GNOMESESS $_ foreach(@G2H);
				close($GNOMESESS);
			}
		}
	}
	# Fluxbox
	if(-d "$ENV{HOME}/.fluxbox") {
		my @FluxboxStartup;
		push(@FluxboxStartup, "$DaemonExec &");
		if(-e "$ENV{HOME}/.fluxbox/startup") {
			open(my $OLDFLUX, '<', "$ENV{HOME}/.fluxbox/startup");
			push(@FluxboxStartup, $_) while(<$OLDFLUX>);
			close($OLDFLUX);
		}
		open(my $FLUXSTART, '>', "$ENV{HOME}/.fluxbox/startup");
		foreach(@FluxboxStartup) {
			chomp;
			print $FLUXSTART "$_\n";
		}
		close($FLUXSTART);
	}
	return($RETURN);
}

# Purpose: Wrapper around AddAutostart that detects if we have an pre-2.14
#		version of GNOME and displays an information dialog about the
#		inability to add a gnome autostart
# Usage: DP_AddAutostart();
sub DP_AddAutostart {
	my $GNOME_Version;
	my $Old_GNOME;

	# Set the settings var
	$InternalConfig{AutostartOn} = 1;

	# Try to get the gnome version from the gnome control center
	foreach(split(/:/, sprintf('%s:%s', $FindBin::RealBin, $ENV{PATH} ))) {
		if (-x "$_/gnome-control-center") {
			$GNOME_Version = qx#$_/gnome-control-center --version#;
			chomp($GNOME_Version);
			$GNOME_Version =~ s/^(.+)\s+(.+)\s+(.+)$/$3/;
			$GNOME_Version =~ s/^(\d+\.\d+).*$/$1/;
			$GNOME_Version = undef unless($GNOME_Version =~ /^\d+\.\d+(\.\d+.*)?$/);
			last;
		}
	}
	if(defined($GNOME_Version)) {
		unless($GNOME_Version > 2.12) {
			$Old_GNOME = 1;
		} else {
			$Old_GNOME = 0;
		}
	} else {	# If we couldn't get the GNOME version then we just assume the user 
			# either doesn't use GNOME or has a version of GNOME newer than 2.12
		$Old_GNOME = 0;
	}

	my $Result = AddAutostart($Old_GNOME);
	return(1) if $Result eq 'okay';

	if($Result eq 'gnome-fail') {
		# GNOME failed, check if GNOME_DESKTOP_SESSION_ID is set.
		# If it isn't set, then the user isn't running GNOME *now* so we don't want to display the
		# error.
		if(defined($ENV{GNOME_DESKTOP_SESSION_ID}) and length($ENV{GNOME_DESKTOP_SESSION_ID})) {
			# It's set, so the user is running GNOME - display the error
			# TRANSLATORS: This string appears when Day Planner fails to add dayplanner-daemon
			# 	to autostart in old GNOME releases (version 2.12 and older).
			# 	I am aware of the fact that it may not apply everywhere, but it's
			# 	about as close as it gets, and users running GNOME 2.13 and later or
			# 	not running GNOME at all will not see this.
			DPError($i18n->get("A problem occurred while setting up automatic startup of the Day Planner reminder.\n\nSet it up manually by selecting: menu -> desktop -> settings -> sessions. From there select \"Startup Programs\", click \"Add\". Type \"dayplanner-daemon\" in the \"Startup Command\" field and press the \"OK\" button."));
		}
	} else {
		DPIntWarn("Unknown return value from AddAutostart(): $Result");
	}
	return(1);
}

# Purpose: Wrapper around RemoveAutostart that sets the config vars
# Usage: DP_RemoveAutostart();
sub DP_RemoveAutostart {
	if(DPQuestion($i18n->get('Disabling automatic startup of the reminder will prevent notifications unless Day Planner has been manually started. Disable automatic startup?'))) {
		$InternalConfig{AutostartOn} = 1;
		return(RemoveAutostart());
	}
	return(undef);
}

# Purpose: Get the number of *milli*seconds until midnight
# Usage: my $miliseconds = MilisecondsUntilMidnight();
sub MilisecondsUntilMidnight {
	my ($currsec,$currmin,$currhour,$currmday,$currmonth,$curryear,$currwday,$curryday,$currisdst) = localtime(time);
	# Convert hours to seconds
	$currhour = $currhour * 60 * 60;
	# Minutes to seconds
	$currmin = $currmin * 60;

	my $SecondsInADay = 86_400;
	my $total = $SecondsInADay - ($currhour + ($currmin + $currsec));
	return($total*1000);
}

# Purpose: Repopulate the UpcomingEvents widget on a timer (every 24h)
# Usage: \&Day_Changed_Event;
sub Day_Changed_Event {
	# First repopulate the upcoming events widget
	PopulateUpcomingEvents();

	# This should never be called at other times than after midnight, but to be sure we subtract 200
	# from the time value in order to get "yesterday"
	my ($yestersec,$yestermin,$yesterhour,$yestermday,$yestermonth,$yesteryear,$yesterwday,$yesteryday,$yesterisdst) = GetDate(time - 200);
	my ($CalYear, $CalMonth, $CalDay) = $CalendarWidget->get_date();
	if($CalYear == $yesteryear && $CalMonth == $yestermonth && $CalDay == $yestermday) {
		my ($currsec,$currmin,$currhour,$currmday,$currmonth,$curryear,$currwday,$curryday,$currisdst) = GetDate(time - 200);
		$CalendarWidget->select_month($currmonth,$curryear);
		$CalendarWidget->select_day($currmday);
		CalendarChange()
	}

	# Now reset the timer
	Set_DayChangeTimer();
	# Return false to make Glib remove the old timer
	return(0);
}

# Purpose: Set the day-changed timer
# Usage: Set_DayChangeTimer();
sub Set_DayChangeTimer {
	# Set the timer
	Glib::Timeout->add(MilisecondsUntilMidnight(), \&Day_Changed_Event);
}

# Purpose: Find out if a command is in PATH or not
# Usage: InPath(COMMAND);
sub InPath {
	foreach (split /:/, $ENV{PATH}) { if (-x "$_/@_" and ! -d "$_/@_" ) {   return 1; } } return 0;
}

# =============================================================================
# IMPORT/EXPORT
# =============================================================================

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Various utility functions
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Commandline wrapper around the export functions
# Usage: 'x|yzx' => \&CLI_Export,
sub CLI_Export {
	# First initialize the i18n system
	DP_InitI18n();
	# Then initialize the data
	my($Type,$Target) = @_;
	MainInit(1);
	if($Type =~ /(ical|ics_dps)/) {
		runtime_use('File::Basename');
		unless(-w dirname($Target)) {
			die("Unable to write to " . dirname($Target) . "\n");
		}
		if(-d $Target) {
			die("$Target: is a directory\n");
		}
		if($Type =~ /(ical|ics_dps)/) {
			if($iCalendar->write($Target)) {
				print "iCalendar data written to $Target\n";
			}
		}
	} elsif($Type =~ /(html|php)/) {
		if(-e $Target) {
			unless(-w $Target) {
				die("I don't have write permission to $Target\n");
			}
			unless(-d $Target) {
				die("$Target: is not a directory\n");
			}
		}
		if($Type =~ /html/) {
			if(HTML_Export($Target)) {
				print "HTML written to $Target\n";
			}
		} elsif ($Type =~ /php/) {
			if(PHP_Export($Target)) {
				print "PHP written to $Target\n";
			}
		}
	} else {
		die("Unknown type given as argument to CLI_Export: $Type\n");
	}
	exit(0);
}

# Purpose: Commandline wrapper around the import functions
# Usage: 'x|yzx' => \&CLI_Import,
sub CLI_Import {
	my($Type,$Source) = @_;
	MainInit(1);
	die("$Source: does not exist\n") unless(-e $Source);
	die("$Source: is not readable by me\n") unless(-r $Source);
	die("$Source: is a directory\n") if (-d $Source);
	# Initialize the daemon (we need it)
	DaemonInit() or die();
	if($Type =~ /ical/) {
		if($iCalendar->addfile($Source)) {
			print "Imported iCalendar data from $Source\n";
		} else {
			print "Importing failed.\n";
		}
	}
	# Save the data
	SaveMainData();
	# Close the daemon connection
	CloseIPC();
	# Exit peacefully
	exit(0);
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# HTML/PHP export functions 
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Encode special HTML entities
# Usage: HTML_Encode(STRING);
sub HTML_Encode {
	my $String = shift;
	study($String);
	$String =~ s#\n#<br />#g;
	$String =~ s/&/&amp;/g;
	$String =~ s/</&lt;/g;
	$String =~ s/>/&gt;/g;
	$String =~ s/"/&quot;/g;
	return($String);
}

# Purpose: Output the header for all Day Planner HTML files
# Usage: print $FILE HTML_Header(YEAR,DATE,NONDATEMODE?);
sub HTML_Header {
	return('') if $HTML_PHP;
	my ($Year,$Date,$NonDateMode) = @_;
	my $Header = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
	$Header .= "<html><head>\n";
	$Header .= '<meta content="text/html; charset=iso-8859-1" http-equiv="content-type" />';
	$Header .= "<meta name='generator' content='Day Planner $Version - http://www.day-planner.org' />\n";;
	if($NonDateMode) {
		$Header .= "<title>" . HTML_Encode($Date) . "</title>";
	} else {
		$Header .= '<title>' . HTML_Encode($i18n->get_advanced('Day Planner for %(date)', { date => $Date})) . '</title>';
	}
	$Header .= "</head>\n";
	$Header .= '<body>';
	$Header .= '<h2>' . HTML_Encode($Date) . '</h2>';
	$Header .= "<!-- Generated by Day Planner version $Version - $RCSRev -->\n";
	$Header .= HTML_Menu(@_);
	$Header .= '<hr />';
	return($Header);
}

# Purpose: Print the menu for a Day Planner HTML doc
# Usage: print $FILE HTML_Menu(YEAR,DATE,NONDATEMODE?);
sub HTML_Menu {
	my ($Year,$Date,$NonDateMode) = @_;
	my $Menu = '';
	my %Links;
	if($HTML_PHP) {
		# PHP-style links
		%Links = (
			1 => 'index.php?year=<?php print "$Year" ?>&amp;month=1',
			2 => 'index.php?year=<?php print "$Year" ?>&amp;month=2',
			3 => 'index.php?year=<?php print "$Year" ?>&amp;month=3',
			4 => 'index.php?year=<?php print "$Year" ?>&amp;month=4',
			5 => 'index.php?year=<?php print "$Year" ?>&amp;month=5',
			6 => 'index.php?year=<?php print "$Year" ?>&amp;month=6',
			7 => 'index.php?year=<?php print "$Year" ?>&amp;month=7',
			8 => 'index.php?year=<?php print "$Year" ?>&amp;month=8',
			9 => 'index.php?year=<?php print "$Year" ?>&amp;month=9',
			10 => 'index.php?year=<?php print "$Year" ?>&amp;month=10',
			11 => 'index.php?year=<?php print "$Year" ?>&amp;month=11',
			12 => 'index.php?year=<?php print "$Year" ?>&amp;month=12',
			yearindex => 'index.php?page=yearindex',
		);
	} else {
		# HTML-style links
		%Links = (
			1 => "january-$Year.html",
			2 => "february-$Year.html",
			3 => "march-$Year.html",
			4 => "april-$Year.html",
			5 => "may-$Year.html",
			6 => "june-$Year.html",
			7 => "july-$Year.html",
			8 => "august-$Year.html",
			9 => "september-$Year.html",
			10 => "october-$Year.html",
			11 => "november-$Year.html",
			12 => "december-$Year.html",
			yearindex => 'index.html',
		);
	}
	if(not defined($NonDateMode) or not $NonDateMode eq 'M') {
		
#	if(defined($NonDateMode) and not $NonDateMode eq "M") {
#		$Menu .= HTML_Encode($i18n->get("Tools")) . ": <a href='$Links{yearindex}'>" . HTML_Encode($i18n->get("View the main page")) . "</a><br/>\n";
#	} elsif(not defined($NonDateMode) or not $NonDateMode eq "M") {
		$Menu .= HTML_Encode($i18n->get("Tools")) . ": <a href='$Links{yearindex}'>" . HTML_Encode($i18n->get("Change to another year")) . "</a> (" . HTML_Encode($i18n->get_advanced("current: %(year)", { year => $Year })) . ") - </a><br />\n";
		$Menu .= HTML_Encode($i18n->get("Months")) .
			": <a href='$Links{1}'>" . HTML_Encode($i18n->get_month(1)) .
			"</a> - <a href='$Links{2}'>" . HTML_Encode($i18n->get_month(2)).
			"</a> - <a href='$Links{3}'>" . HTML_Encode($i18n->get_month(3)) .
			"</a> - \n<a href='$Links{4}'>" . HTML_Encode($i18n->get_month(4)) .
			"</a> - <a href='$Links{5}'>" . HTML_Encode($i18n->get_month(5)) .
			"</a> - <a href='$Links{6}'>" . HTML_Encode($i18n->get_month(6)) .
			"</a> - \n<a href='$Links{7}'>" . HTML_Encode($i18n->get_month(7)) .
			"</a> - <a href='$Links{8}'>" . HTML_Encode($i18n->get_month(8)) .
			"</a> - <a href='$Links{9}'>" . HTML_Encode($i18n->get_month(9)) .
			"</a> - \n<a href='$Links{10}'>" . HTML_Encode($i18n->get_month(10)) .
			"</a> - <a href='$Links{11}'>" . HTML_Encode($i18n->get_month(11)) .
			"</a> - <a href='$Links{12}'>" . HTML_Encode($i18n->get_month(12)) . "</a><br/>\n";
	}
	return($Menu);
}

# Purpose: Print the footer of all Day Planner HTML documents
# Usage: print $FILE HTML_Footer();
sub HTML_Footer {
	return('') if $HTML_PHP;
	my $Footer = '<br /><small><small>' . HTML_Encode($i18n->get('Generated by')) .  ' <a href="http://www.day-planner.org/">' . HTML_Encode($i18n->get('Day Planner')) . '</a> ' . HTML_Encode($i18n->get('version')) . " $Version</small></small><br />";
	$Footer .= "</body></html>";
	return($Footer);
}

# Purpose: Output a specified day to HTML
# Usage: HTML_DayToHtml(YEAR,MONTH,DAY,DIRECTORY);
sub HTML_DayToHtml {
	my ($Year,$Month,$Day,$Directory) = @_;
	my (@AllDay, @OtherEvents);
	my %WrittenEventList;
	open(my $FILE, '>', "$Directory/dp_$Year$Month$Day.html") or do {
		DPIntWarn("Unable to open $Directory/dp_$Year$Month$Day.html for writing: $!");
		DPError($i18n->get_advanced("Unable to open %(file) for writing: %(error)", { file => "$Directory/dp_$Year$Month$Day.html", error => $!}));
		return(undef);
	};
	# Header
	print $FILE HTML_Header($Year,"$Day-$Month-$Year");
	print $FILE '<table style="text-align: left;" border="1" cellpadding="2" cellspacing="2">';
	print $FILE '<tbody><tr><td>' . HTML_Encode($i18n->get('Time')) . '</td><td>' . HTML_Encode($i18n->get('Description')) . "</td></tr>\n";
	# Write allday and birthdays
	foreach my $Event (sort (@{$iCalendar->get_timeinfo($Year,$Month,$Day,'DAY')})) {
		if(not($WrittenEventList{$Event}))
		{
			print $FILE '<tr><td></td><td>' . HTML_Encode(GetSummaryString($Event,false,$Year,$Month));
			my $EventInfo = $iCalendar->get_info($Event);
			if(defined($EventInfo->{DESCRIPTION})) {
				my $HTML_Fulltext = HTML_Encode($EventInfo->{DESCRIPTION});
				print $FILE "<br /><i>$HTML_Fulltext</i>";
			}
			print $FILE "</td></tr>\n";
			$WrittenEventList{$Event} = true;
		}
	}
	# Write others
	foreach my $Time (sort(@{$iCalendar->get_dateinfo($Year,$Month,$Day)})) {
		next if $Time eq 'DAY';
		foreach my $Event (sort (@{$iCalendar->get_timeinfo($Year,$Month,$Day,$Time)})) {
			if(not($WrittenEventList{$Event}))
			{
				print $FILE "<tr><td>$Time</td><td>" . HTML_Encode(GetSummaryString($Event,false,$Year,$Month));
				my $EventInfo = $iCalendar->get_info($Event);
				if(defined($EventInfo->{DESCRIPTION})) {
					my $HTML_Fulltext = HTML_Encode($EventInfo->{DESCRIPTION});
					print $FILE "<br /><i>$HTML_Fulltext</i>";
				}
				print $FILE "</td></tr>\n";
			}
			$WrittenEventList{$Event} = true;
		}
	}
	# Prepare and write holidays
	unless(defined($HolidayParser)) {
		$HolidayParser = Date::HolidayParser->new($HolidayFile);
	}
	unless(defined($Holidays{$Year})) {
		$Holidays{$Year} = $HolidayParser->get($Year);
	}
	if(defined($Holidays{$Year}) and defined($Holidays{$Year}->{$Month}) and defined($Holidays{$Year}->{$Month}{$Day})) {
		foreach my $CurrHoliday (keys(%{$Holidays{$Year}->{$Month}{$Day}})) {
			print $FILE '<tr><td></td><td> ' . HTML_Encode($CurrHoliday) . "</td></tr>\n";
		}
	}
	print $FILE '</tbody></table>';
	print $FILE HTML_Footer();
	close($FILE);
}

# Purpose: Output a specified month in HTML
# Usage: HTML_MonthToHtml
sub HTML_MonthToHtml {
	my ($Year,$Month,$Directory) = @_;
	my %RawMonthNames = (
		1 => 'january',
		2 => 'february',
		3 => 'march',
		4 => 'april',
		5 => 'may',
		6 => 'june',
		7 => 'july',
		8 => 'august',
		9 => 'september',
		10 => 'october',
		11 => 'november',
		12 => 'december',
	);
	open(my $FILE, '>', "$Directory/$RawMonthNames{$Month}-$Year.html") or do {
		DPIntWarn("Unable to open $Directory/$RawMonthNames{$Month}-$Year.html for writing: $!");
		DPError($i18n->get_advanced("Unable to open %(file) for writing: %(error)", { file => "$RawMonthNames{$Month}-$Year.html", error => $!}));
		return(undef);
	};
	print $FILE HTML_Header($Year, $i18n->get_month($Month) . " $Year");
	my $HadContent;
	my $MonthInfo = $iCalendar->get_monthinfo($Year,$Month);
	foreach my $Day (sort @{$MonthInfo}) {
		$HadContent = 1;
		print $FILE "<a href='dp_$Year$Month$Day.html'>" . HTML_Encode("$Day. " . $i18n->get_month($Month) ." $Year") . "</a><br/>\n";
	}
	unless($HadContent) {
		print $FILE '<i>' . HTML_Encode($i18n->get('There are no events this month')) . '</i>';
	}
	# Prepare and write holidays
	unless(defined($HolidayParser)) {
		$HolidayParser = Date::HolidayParser->new($HolidayFile);
	}
	unless(defined($Holidays{$Year})) {
		$Holidays{$Year} = $HolidayParser->get($Year);
	}
	print $FILE HTML_Footer();
	close($FILE);
}

# Purpose: Output all birthdays to a file
# Usage: HTML_BirthdayList(DIRECTORY);
sub HTML_BirthdayList {
	warn("HTML_BirthdayList: STUBBED, using \%BirthdayContents!");
=cut
	my($Directory) = @_;
	open(my $FILE, '>', "$Directory/birthdays.html") or do {
		DPIntWarn("Unable to open $Directory/birthdays.html for writing: $!");
		DPError(sprintf($i18n->get("Unable to open %s for writing: %s"), "$Directory/birthdays.html", $!));
		return(undef);
	};
	print $FILE HTML_Header(undef,$i18n->get("Birthdays"),1);
	if(keys(%BirthdayContents)) {
		print $FILE '<table style="text-align: left;" border="1" cellpadding="2" cellspacing="2">';
		print $FILE "<tbody><tr><td>" . HTML_Encode($i18n->get("Date")) . "</td><td>" . HTML_Encode($i18n->get("Name")) . "</td></tr>\n";
		foreach my $Month (sort {$a <=> $b}  keys(%BirthdayContents)) {
			foreach my $Day (sort {$a <=> $b} keys(%{$BirthdayContents{$Month}})) {
				my $PrintDay = AppendZero($Day);
				foreach my $Name (sort(keys(%{$BirthdayContents{$Month}{$Day}}))) {
					print $FILE "<tr><td>" . HTML_Encode("$PrintDay $MonthNames{$Month}") . "</td><td>" . HTML_Encode($Name) . "</td></tr>\n";
				}
			}
		}
		print $FILE '</tbody></table>';
	} else {
		print $FILE . "<i>" .  HTML_Encode($i18n->get("No birthdays are defined.")) . "</i><br/>";
	}
	print $FILE HTML_Footer();
=cut
}

# Purpose: Output a year information page to HTML
# Usage: HTML_YearHtml(YEAR,DIRECTORY);
sub HTML_YearHtml {
	my ($Year, $Directory) = @_;
	open(my $FILE, '>', "$Directory/$Year.html") or do {
		DPIntWarn("Unable to open $Directory/$Year.html for writing: $!");
		DPError($i18n->get_advanced("Unable to open %(file) for writing: %(error)", { file => "$Directory/$Year.html", error => $!}));
		return(undef);
	};
	print $FILE HTML_Header($Year, $Year, 'Y');
	print $FILE HTML_Encode($i18n->get('Select the month to view in the list above')) . "<br />\n";
	print $FILE HTML_Footer();
}

# Purpose: Output a list of years (aka. the index page) to HTML
# Usage: HTML_YearList(DIRECTORY);
sub HTML_YearList {
	my ($Directory) = @_;
	open(my $FILE, '>', "$Directory/index.html") or do {
		DPIntWarn("Unable to open $Directory/index.html for writing: $!");
		DPError($i18n->get_advanced("Unable to open %(file) for writing: %(error)", { file => "$Directory/index.html", error => $!}));
		return(undef);
	};
	print $FILE HTML_Header("", "Day Planner", "M");
	print $FILE HTML_Encode($i18n->get("Select the year to view:")) . "<br />\n";
	foreach(@{$iCalendar->get_years()}) {
		print $FILE "<a href='$_.html'>" . HTML_Encode($_) . "</a><br />\n";
	}
	print $FILE HTML_Footer();
}

# Purpose: Output a php file that adds autodetection of todays date to
#	Day Planner HTML exports. (NOTE: Not used for actual PHP exporting)
# Usage: HTML_PHPIndex(DIRECTORY);
sub HTML_PHPIndex {
	my ($Directory) = @_;
	open(my $FILE, '>', "$Directory/index.php") or do {
		DPIntWarn("Unable to open $Directory/index.php for writing: $!");
		DPError($i18n->get_advanced("Unable to open %(file) for writing: %(error)", { file => "$Directory/index.php", error => $!}));
		return(undef);
	};
	print $FILE "<?php\n// This is a simple script written by Day Planner to add autodetection of\n// the current day to exported Day Planner HTML sites. Only useful on\n// webservers with php support.\n// See also Day Planners php export feature for a more complete\n// PHP export of Day Planner data.\n// Copyright (C) Eskild Hustvedt 2006. Licensed under the same license as Day Planner\n";
	print $FILE HTML_PHP_DayDetectFunc();
	print $FILE '$file = DayDetectFunc("./");' . "\n";
	print $FILE 'if($file) {' . "\n";
	print $FILE "\t" . 'include($file);' . "\n";
	print $FILE "} else {" . "\n";
	print $FILE "\t" . 'print("Unable to detect files. This export is corrupt!");' . "\n";
	print $FILE "}\n?>";
}

# Purpose: Export Day Planner data to HTML
# Usage: HTML_Export(DIRECTORY);
sub HTML_Export {
	my %RawMonthNames = (
		1 => 'january',
		2 => 'february',
		3 => 'march',
		4 => 'april',
		5 => 'may',
		6 => 'june',
		7 => 'july',
		8 => 'august',
		9 => 'september',
		10 => 'october',
		11 => 'november',
		12 => 'december',
	);
	my $Dir = $_[0];
	unless(-d $Dir) {
		runtime_use('File::Path');
		eval("File::Path::mkpath('$Dir')");
		if($@) {
			DPIntWarn("Unable to mkpath($Dir): $@");
			DPError($i18n->get_advanced("Unable to create the directory %(dir): %(error)", { dir => $Dir, error => $@}));
			return(undef);
		}
	}
	foreach my $Year (@{$iCalendar->get_years}) {
		HTML_YearHtml($Year,$Dir);
		foreach my $Month (@{$iCalendar->get_months($Year)}) {
			foreach my $Day (@{$iCalendar->get_monthinfo($Year,$Month)}) {
				HTML_DayToHtml($Year, $Month, $Day, $Dir);
			}
		}
		foreach(1..12) {
			HTML_MonthToHtml($Year,$_,$Dir);
		}
	}
	HTML_YearList($Dir);
	HTML_PHPIndex($Dir);
	# TODO: Either FIXME or DROPME!
#	HTML_BirthdayList($Dir);
}

# Purpose: Function to detect todays day using php
# Usage: print HTML_PHP_DayDetectFunc();
sub HTML_PHP_DayDetectFunc {
	my $Return = 'function DayDetectFunc ($datadir) {' . "\n";
	$Return .= "\t" . '$Year = date("Y");' . "\n";
	$Return .= "\t" . '$Month = date("n");' . "\n";
	$Return .= "\t" . '$Day = date("j");' . "\n";
	$Return .= "\t" . '$Months = array(1 => "january", 2 => "february", 3 => "march", 4 => "april", 5 => "may", 6 => "june", 7 => "july", 8 =>"august", 9 =>"september", 10 => "october", 11 => "november", 12 =>"december");' . "\n";
	$Return .= "\t" . 'if(file_exists("$datadir/dp_$Year$Month$Day.html")) {' . "\n";
	$Return .= "\t\t" . 'return("$datadir/dp_$Year$Month$Day.html");' . "\n";
	$Return .= "\t" . '} elseif(file_exists("$datadir/$Months[$Month]-$Year.html")) {' . "\n";
	$Return .= "\t\t" . 'return("$datadir/$Months[$Month]-$Year.html");' . "\n";
	$Return .= "\t" . '} elseif(file_exists("$datadir/index.html")) {' . "\n";
	$Return .= "\t\t" . 'return("$datadir/index.html");' . "\n";
	$Return .= "\t} else {" . "\n";
	$Return .= "\t\treturn 0;\n";
	$Return .= "\t}\n}\n";
	return($Return);
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Plan migration functions
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Purge the current $PlanConvertHash-> buffer and put it into %CalendarContents
# Usage: PlanConvert_PurgeBuffer(\%PlanConvertHash);
sub PlanConvert_PurgeBuffer {
	my $PlanConvertHash = $_[0];
	my $ErrorMessage;
	if(defined($PlanConvertHash->{Day})) {
		if ($PlanConvertHash->{Type} eq 'normal') {
			my %TargetEvent;
			$TargetEvent{DTSTART} = iCal_GenDateTime($PlanConvertHash->{Year},$PlanConvertHash->{Month},$PlanConvertHash->{Day},$PlanConvertHash->{Time});
			$TargetEvent{DTEND} = $TargetEvent{DTSTART};
			delete($PlanConvertHash->{Day});
			delete($PlanConvertHash->{Month});
			delete($PlanConvertHash->{Year});
			delete($PlanConvertHash->{Time});
			if (not defined($PlanConvertHash->{Summary})) {
				DPIntWarn("Plan importer: Event at $TargetEvent{DTSTART} did not have a summary. Skipping.");
				return(undef);
			}
			$TargetEvent{SUMMARY} = $PlanConvertHash->{Summary};
			if (defined($PlanConvertHash->{Fulltext})) {
				$TargetEvent{DESCRIPTION} = $PlanConvertHash->{Fulltext};
				delete($PlanConvertHash->{Fulltext});
			}
			delete($PlanConvertHash->{Summary});
			$iCalendar->add(%TargetEvent);
		} elsif ($PlanConvertHash->{Type} eq 'bday') {
			my %TargetEvent;
			$TargetEvent{DTSTART} = iCal_GenDateTime($PlanConvertHash->{Year},$PlanConvertHash->{Month},$PlanConvertHash->{Day});
			$TargetEvent{DTEND} = $TargetEvent{DTSTART};
			delete($PlanConvertHash->{Day});
			delete($PlanConvertHash->{Month});
			delete($PlanConvertHash->{Year});
			if (not defined($PlanConvertHash->{Summary})) {
				DPIntWarn("Plan importer: Event at $TargetEvent{DTSTART} did not have a summary. Skipping.");
				next;
			}
			$TargetEvent{'X-DP-BIRTHDAY'} = 'TRUE';
			$TargetEvent{'X-DP-BIRTHDAYNAME'} = $PlanConvertHash->{Summary};
			$TargetEvent{RRULE} = 'FREQ=YEARLY';
			$TargetEvent{SUMMARY} = $i18n->get_advanced("%(name)'s birthday",  { name => $PlanConvertHash->{Summary}});
			delete($PlanConvertHash->{Fulltext});
			delete($PlanConvertHash->{Summary});
			$iCalendar->add(%TargetEvent);
		} else {
			DPIntWarn("BUG!!! Invalid type: $PlanConvertHash->{Type}. This could result in dangerous errors.");
		}
		$PlanConvertHash->{Type} = 'normal';
	}
}

# Purpose: Convert the file supplied
# Usage: PlanConvert_PurgeBuffer(/path/to/file,\%PlanConvertHash, $ProgressBar);
sub PlanConvert_ProcessFile {
	my $PlanConvertHash = $_[1]; 
	$PlanConvertHash->{Type} = 'normal';

	open(my $PLAN_FILE, '<', $_[0]) or do {
		DPIntWarn("Unable to open $_[0]: $!");
		return(0);
	};
	my $LineNo = 0;
	while(<$PLAN_FILE>) {
		$LineNo++;
		next if /^\s*(O|o|t|e|l|a|y|P|p|m|L|u|E)/;	# These are Plan specific stuff, just ignore them
		chomp;
		if (/^\s*(N)/) {		# Entry equalent to the dayplanner summary
			my $Summary = $_;
			$Summary =~ s#^\s*N\s+(.*)#$1#;
			$PlanConvertHash->{Summary} = $Summary;
		} elsif (/^\s*(M)/) {		# Entry equalent to the dayplanner fulltext
			my $Fulltext = $_;
			$Fulltext =~ s#^\s*M\s+(.*)#$1#;
			if(defined($PlanConvertHash->{Fulltext})) {
				$PlanConvertHash->{Fulltext} = "$PlanConvertHash->{Fulltext} $Fulltext";
			} else {
				$PlanConvertHash->{Fulltext} = $Fulltext;
			}
		} elsif (/^\s*(R)/) {		# These we just skip but might parse at some point
			if (/^\s*R\s+0\s+0\s+0\s+0\s+1/) {
				$PlanConvertHash->{Type} = 'bday';
			} else {
				next;
			}
		} elsif (/^\s*\d/) {		# Okay, it starts with a digit, it's a new date
			PlanConvert_PurgeBuffer($PlanConvertHash);
			my ($Day,$Month,$Year,$Time) = ($_,$_,$_,$_);
			# Get the day
			$Day =~ s#^\s*\d+/(\d+)/.*#$1#;
			# Get the month
			$Month =~ s#^\s*(\d+)/\d+/.*#$1#;
			# Get the year
			$Year =~ s#^\s*\d+/\d+/(\d+)\s+.*#$1#;
			# Get the time
			$Time =~ s#\s*\d+/\d+/\d+\s+(\d+:\d+):\d+\s+.*#$1#;
			# Convert the time to a more dayplannerish format
			$Time = '00:00' if $Time eq '99:99';
			if ($Time =~ /^\d+:\d$/) {
				$Time = $Time . '0';
			}
			if ($Time =~ /^\d:\d*$/) {
				$Time = "0$Time";
			}
			# Set the variables in the hash
			$PlanConvertHash->{Day} = $Day;
			$PlanConvertHash->{Month} = $Month;
			$PlanConvertHash->{Year} = $Year;
			$PlanConvertHash->{Time} = $Time;
		} else {
			DPIntWarn("WARNING: Unrecognized line (please report this): $_[0]:$LineNo: $_");
			next;
		}
	}
	return(1);
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# GUI import/export dialogs and helper functions
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Pop up a graphical dialog for importing data
# Usage: ImportData();
sub ImportData {
	$MainWindow->set_sensitive(0);
	# Create the window and VBox
	my $ImportWindow = Gtk2::Window->new();
	$ImportWindow->set_modal(1);
	$ImportWindow->set_transient_for($MainWindow);
	$ImportWindow->set_position('center-on-parent');
	$ImportWindow->set_title($i18n->get('Import data'));
	$ImportWindow->set_resizable(0);
	$ImportWindow->set_border_width(12);
	$ImportWindow->set_skip_taskbar_hint(1);
	$ImportWindow->set_skip_pager_hint(1);
	$ImportWindow->signal_connect('destroy' => sub {
			$MainWindow->set_sensitive(1);
		});
	my $ImportVBox = Gtk2::VBox->new();
	$ImportWindow->add($ImportVBox);

	# Create the initial text
	my $ImportLabel = Gtk2::Label->new($i18n->get('Import data from:'));
	$ImportVBox->pack_start($ImportLabel,0,0,0);

	# Create the radio buttons for selection and pack them onto the VBox
	my %StateHash;
	my $ActiveButton = 'dayplanner';
	my $FromFileButton = Gtk2::RadioButton->new (undef, $i18n->get('A file'));
	my $FromProgramButton = Gtk2::RadioButton->new ($FromFileButton, $i18n->get('Other applications'));
	$ImportVBox->pack_start($FromFileButton,0,0,0);
	$ImportVBox->pack_start($FromProgramButton,0,0,0);

	# Add the buttons
	my $ButtonHBox = Gtk2::HBox->new();
	$ButtonHBox->show();
	$ImportVBox->pack_end($ButtonHBox,0,0,0);

	# Ok button
	my $OKButton = Gtk2::Button->new_from_stock('gtk-ok');
	$OKButton->show();
	$OKButton->can_default(1);
	$ImportWindow->set_default($OKButton);
	# Signal callback (starts a function depending on the radio button selected)
	$OKButton->signal_connect('clicked' => sub {
			$ImportWindow->destroy();
			if($FromFileButton->get_active) {
				ImportDataFromFile();
			} elsif ($FromProgramButton->get_active) {
				ImportDataFromProgram();
			} else {
				DPIntWarn("Unknown active button!\n");
			}
		});
	$ButtonHBox->pack_end($OKButton,0,0,0);

	# Cancel button
	my $CancelButton = Gtk2::Button->new_from_stock('gtk-cancel');
	$CancelButton->show();
	$ButtonHBox->pack_end($CancelButton,0,0,0);
	# Signal callback (destroys the window)
	$CancelButton->signal_connect('clicked' => sub {
			$ImportWindow->destroy();
		});

	# Add tooltips
	my $Tooltips = Gtk2::Tooltips->new();
	$Tooltips->set_tip($FromFileButton, $i18n->get('Import data from a file'));
	$Tooltips->set_tip($FromProgramButton, $i18n->get('Import data from various other applications'));
	$Tooltips->set_tip($CancelButton, $i18n->get('Cancel importing and return to Day Planner'));
	$Tooltips->set_tip($OKButton, $i18n->get('Continue'));

	# Show it all
	$Tooltips->enable();
	$FromFileButton->show();
	$FromProgramButton->show();
	$ImportLabel->show();
	$ImportVBox->show();
	$ImportWindow->show();
}

# Purpose: Pop up a import dialog for importing from other programs
# Usage: ImportDataFromProgram(IS_FIRST_TIME);
sub ImportDataFromProgram {
	my $FirstTime = $_[0];
	my $ProgressWindow = DPCreateProgressWin($i18n->get('Preparing'), $i18n->get('Preparing to import'), 0);

	my %Programs = (
		Evolution => 0,
		Plan => 0,
		GnomeCalendar => 0,
		Korganizer => 0,
		Orage => 0,
		ImportPossible => 0,
		TotalPrograms => 5,
	);

	# Detect evolution
	if(-e "$ENV{HOME}/.evolution/calendar/local/system/calendar.ics") {
		$Programs{Evolution} = 1;
		$Programs{ImportPossible} = 1;
	}
	ProgressMade($Programs{TotalPrograms}, 1, $ProgressWindow);

	# Detect Gnome Calendar
	if(-e "$ENV{HOME}/.gnome/user-cal.vcf") {
		$Programs{GnomeCalendar} = 1;
		$Programs{ImportPossible} = 1;
	}
	ProgressMade($Programs{TotalPrograms}, 2, $ProgressWindow);

	# Detect plan
	foreach my $Dir ("$ENV{HOME}/.plan","$ENV{HOME}/.plan.dir") {
		if (defined($Dir) and -d $Dir and -e "$Dir/dayplan") {
			$Programs{ImportPossible} = 1;
			$Programs{Plan} = 1;
		}
	}
	ProgressMade($Programs{TotalPrograms}, 3, $ProgressWindow);
	# Detect Korganizer
	if (-e "$ENV{HOME}/.kde/share/apps/korganizer/std.ics") {
		$Programs{Korganizer} = 1;
		$Programs{ImportPossible} = 1;
	}
	ProgressMade($Programs{TotalPrograms}, 4, $ProgressWindow);
	# Detect Orage
	if (-e "$ENV{HOME}/.config/xfce4/orage/orage.ics") {
		$Programs{Orage} = 1;
		$Programs{ImportPossible} = 1;
	}
	ProgressMade($Programs{TotalPrograms}, 5, $ProgressWindow);

	$ProgressWindow->{Window}->destroy();
	if(not $Programs{ImportPossible}) {
		if(not $FirstTime) {
			$MainWindow->show();
			DPInfo($i18n->get('Could not detect data from any application to import.'));
		}
		return(0);
	}
	my %ImportFrom;
	$MainWindow->set_sensitive(0) if($MainWindow);

	# Create the window and vbox
	my $ImportWindow = Gtk2::Window->new();
	$ImportWindow->set_modal(1);
	$ImportWindow->set_transient_for($MainWindow) if $MainWindow;
	$ImportWindow->set_position('center-on-parent');
	$ImportWindow->set_default_size(120,50);
	$ImportWindow->resize(120,50);
	# Do some extra things if it is the first time
	if($FirstTime) {
		$ImportWindow->set_title($i18n->get('Day Planner') . ' - ' . $i18n->get('Import data'));
		# Set the icon
		my $WindowIcon = DetectImage('dayplanner-48x48.png','dayplanner-32x32.png','dayplanner-24x24.png', 'dayplanner-16x16.png', 'dayplanner.png','dayplanner_HC48.png','dayplanner_HC24.png', 'dayplanner_HC16.png', );
		if ($WindowIcon) {
			$ImportWindow->set_default_icon_from_file($WindowIcon);
		}	
	} else {
		$ImportWindow->set_title($i18n->get("Import data"));
		$ImportWindow->set_skip_taskbar_hint(1);
		$ImportWindow->set_skip_pager_hint(1);
	}
	$ImportWindow->set_resizable(0);
	$ImportWindow->set_border_width(12);
	my $ImportVBox = Gtk2::VBox->new();
	$ImportWindow->add($ImportVBox);

	# Create the initial text
	my $LabelText = $FirstTime ? $i18n->get("Welcome to Day Planner.\n\nYou may now choose to import data from other applications.\nCheck the application(s) you want to import from below and\npress Ok to continue, press Cancel if you don't want\nto import anything.") : $i18n->get('Which application(s) do you want to import data from?');
	my $ImportLabel = Gtk2::Label->new($LabelText);
	$ImportLabel->set_line_wrap_mode('word-char');
	$ImportVBox->pack_start($ImportLabel,0,0,0);
	$ImportLabel->show();

	# Evolution
	my $EvolutionButton = Gtk2::CheckButton->new_with_label('Evolution');
	$ImportVBox->pack_start($EvolutionButton,0,0,0);
	$EvolutionButton->show();
	if($Programs{Evolution}) {
		$EvolutionButton->set_active(1);
	} else {
		$EvolutionButton->set_sensitive(0);
	}
	
	# Gnome calendar
	my $GnomeCalButton = Gtk2::CheckButton->new_with_label('Gnome calendar');
	$ImportVBox->pack_start($GnomeCalButton,0,0,0);
	$GnomeCalButton->show();
	if($Programs{GnomeCalendar}) {
		$GnomeCalButton->set_active(1);
	} else {
		$GnomeCalButton->set_sensitive(0);
	}
	
	# Korganizer
	my $KorganizerButton = Gtk2::CheckButton->new_with_label('Korganizer');
	$ImportVBox->pack_start($KorganizerButton,0,0,0);
	$KorganizerButton->show();
	if($Programs{Korganizer}) {
		$KorganizerButton->set_active(1);
	} else {
		$KorganizerButton->set_sensitive(0);
	}
	
	# Orage
	my $OrageButton = Gtk2::CheckButton->new_with_label('Orage');
	$ImportVBox->pack_start($OrageButton,0,0,0);
	$OrageButton->show();
	if($Programs{Orage}) {
		$OrageButton->set_active(1);
	} else {
		$OrageButton->set_sensitive(0);
	}
	
	# Plan
	my $PlanButton= Gtk2::CheckButton->new_with_label('Plan');
	$ImportVBox->pack_start($PlanButton,0,0,0);
	if($Programs{Plan}) {
		$PlanButton->set_active(1);
	} else {
		$PlanButton->set_sensitive(0);
	}
	$PlanButton->show();
	
	# Add the buttons
	my $ButtonHBox = Gtk2::HBox->new();
	$ButtonHBox->show();
	$ImportVBox->pack_end($ButtonHBox,0,0,0);
	
	# Ok button
	my $OKButton = Gtk2::Button->new_from_stock('gtk-ok');
	$OKButton->show();
	$OKButton->can_default(1);
	$ImportWindow->set_default($OKButton);
	# Signal callback (starts a function depending on the radio button selected)
	$OKButton->signal_connect('clicked' => sub {
			$ImportWindow->destroy();
			ImportProgData($EvolutionButton->get_active(), $PlanButton->get_active(), $GnomeCalButton->get_active(), $KorganizerButton->get_active(),$OrageButton->get_active());
			$MainWindow->show() if $FirstTime;
		}	# NOTE: Removing this makes perl 5.8.8 in mdv20070 segfault.
			# This might need a bug report
		);
	$ButtonHBox->pack_end($OKButton,0,0,0);

	# Cancel button
	my $CancelButton = Gtk2::Button->new_from_stock('gtk-cancel');
	$CancelButton->show();
	$ButtonHBox->pack_end($CancelButton,0,0,0);
	# Signal callback (destroys the window)
	$CancelButton->signal_connect('clicked' => sub {
			$ImportWindow->destroy();
			$MainWindow->show() if $FirstTime;
			$MainWindow->set_sensitive(1) if $MainWindow;
		});
	$ImportVBox->show();
	$ImportWindow->show();
	return(1);
}

# Purpose: Import data from other programs
# Usage: ImportProgData(EVOLUTION?, PLAN?, GNOME_CAL?, KORGANIZER?, ORAGE?);
sub ImportProgData {
	my($ImportEvolution, $ImportPlan, $ImportGnome, $ImportKorganizer, $ImportOrage) = @_;
	my (@PlanFiles, @EvolutionFiles);

	my $MaxProgress = 1;
	my $CurrentProgress = 1;
	my $Progress = DPCreateProgressWin($i18n->get("Importing..."), $i18n->get("Preparing"), 0);

	# Get the files to import plan data from
	if($ImportPlan) {
		foreach my $Dir ("$ENV{HOME}/.plan","$ENV{HOME}/.plan.dir") {
			if (defined($Dir) and -d $Dir and -e "$Dir/dayplan") {
				while (my $File = glob("$Dir/*")) {
					next if $File =~ m#/(lock\.pland|pland|holiday)$#;
					push(@PlanFiles, $File);
				}
			}
		}
	}

	# Get the files to import Evolution data from
	if($ImportEvolution) {
		my $EvoDir = "$ENV{HOME}/.evolution/calendar/local/system";
		while(my $File = glob("$EvoDir/*.ics")) {
			push(@EvolutionFiles, $File);
		}
	}

	# Set MaxProgress for gnome cal
	if($ImportGnome) {
		$MaxProgress++;
	}
	# Set MaxProgress for Korganizer (FIXME: Does it have additional files?)
	if($ImportKorganizer) {
		$MaxProgress++;
	}
	# Set the total MaxProgress
	if(@PlanFiles) {
		$MaxProgress += scalar(@PlanFiles);
	}
	if(@EvolutionFiles) {
		$MaxProgress += scalar(@EvolutionFiles);
	}

	# Begin with gnome
	if($ImportGnome) {
		ProgressMade($MaxProgress, $CurrentProgress, $Progress, "Gnome Calendar");
		$iCalendar->addfile("$ENV{HOME}/.gnome/user-cal.vcf");
		$CurrentProgress++;
	}
	# Continue with evolution
	foreach(@EvolutionFiles) {
		ProgressMade($MaxProgress, $CurrentProgress, $Progress, "Evolution");
		$iCalendar->addfile($_);
		$CurrentProgress++;
	}
	# Then do korganizer
	if($ImportKorganizer) {
		ProgressMade($MaxProgress, $CurrentProgress, $Progress, "Korganizer");
		$iCalendar->addfile("$ENV{HOME}/.kde/share/apps/korganizer/std.ics");
		$CurrentProgress++;
	}
	# Then do Orage
	if($ImportOrage) {
		ProgressMade($MaxProgress, $CurrentProgress, $Progress, "Orage");
		$iCalendar->addfile("$ENV{HOME}/.config/xfce4/orage/orage.ics");
		$CurrentProgress++;
	}
	# End with plan
	foreach(@PlanFiles) {
		ProgressMade($MaxProgress, $CurrentProgress, $Progress, "Plan");
		my %PlanConvertHash;
		if(PlanConvert_ProcessFile($_,\%PlanConvertHash)) {
			PlanConvert_PurgeBuffer(\%PlanConvertHash);
		}
		$CurrentProgress++;
	}
	ProgressMade($MaxProgress, $CurrentProgress, $Progress, $i18n->get("Done"));
	UpdatedData(1);
	$Progress->{Window}->destroy();
	
	$MainWindow->set_sensitive(1) if $MainWindow;
}

# Purpose: Pop up a file picker to find the file to import
# Usage: ImportDataFromFile();
sub ImportDataFromFile {
	# NOTE: We currently rely upon file extensions. This might actually be fine
	# but this note is left as a little heads up. As we filter on filenames anyway
	# the only case would be a file named *.ics which should be *.dpf or the other
	# way around.
	# Create the main window
	my $ImportWindow = Gtk2::FileChooserDialog->new('Import data from file', $MainWindow, 'open',
	'gtk-cancel' => 'reject',
	'gtk-open' => 'accept',);
	$ImportWindow->set_local_only(1);
	$ImportWindow->set_default_response('accept');
	my $filter = Gtk2::FileFilter->new;
	$filter->add_pattern('*.ics');
	$filter->add_pattern('*.vcf');
	$filter->set_name($i18n->get('All supported file formats'));
	$ImportWindow->add_filter($filter);
	my $Response = $ImportWindow->run();
	if($Response eq 'accept') {
		my $Filename = $ImportWindow->get_filename();
		if($Filename =~ /\.(ics|vcf)$/i) {
			$iCalendar->addfile($Filename);
			UpdatedData();
		} else {
			DPIntWarn("Unknown filetype: $Filename");
		}
	}
	$ImportWindow->destroy();
}

# Purpose: Pop up a graphical dialog for exporting data
# Usage: ExportData();
sub ExportData {
	# Create the main window
	my $ExportWindow = Gtk2::FileChooserDialog->new($i18n->get('Export data'), $MainWindow, 'save',
	'gtk-cancel' => 'reject',
	'gtk-save' => 'accept',);
	$ExportWindow->set_current_name($i18n->get('My_dayplanner'));
	$ExportWindow->set_local_only(1);

	# Create the file format selection part
	my $ExportHBox = Gtk2::HBox->new();
	$ExportHBox->show();
	my $ExportVBox = Gtk2::VBox->new();
	$ExportVBox->show();
	$ExportWindow->set_extra_widget($ExportVBox);
	$ExportVBox->pack_start($ExportHBox,0,0,0);

	# Encryption checkbox
	#my $EncryptionCheckbox = Gtk2::CheckButton->new($i18n->get("Password protect the file (encryption)"));
	#$EncryptionCheckbox->show();
	#$ExportVBox->pack_end($EncryptionCheckbox,0,0,0);
	# Only birthdays checkbox
	#my $BirthdaysOnlyCheckbox = Gtk2::CheckButton->new($i18n->get("Only export birthdays"));
	#$BirthdaysOnlyCheckbox->show();
	#$ExportVBox->pack_end($BirthdaysOnlyCheckbox,0,0,0);

	# Label
	my $ActiveIndex;
	my $FiletypeLabel = Gtk2::Label->new($i18n->get('Save as filetype:'));
	$FiletypeLabel->set_justify('left');
	$FiletypeLabel->show();
	$ExportHBox->pack_start($FiletypeLabel,0,0,0);
	# Combo selector
	my $Export_Combo = Gtk2::ComboBox->new_text;
	$Export_Combo->insert_text(0, 'iCalendar (*.ics)');
	# TRANSLATORS: Selects where to export to
	$Export_Combo->insert_text(1, $i18n->get('XHTML (exports to a directory)'));
	$ExportHBox->pack_end($Export_Combo,0,0,0);
	$Export_Combo->show();

	# Handle changed values in the combo box
	$Export_Combo->signal_connect('changed' => sub {
		$ActiveIndex = $Export_Combo->get_active();
		#unless($ActiveIndex == 0) {
		#	$EncryptionCheckbox->set_sensitive(0);
		#} else {
		#	$EncryptionCheckbox->set_sensitive(1);
		#}
		if($ActiveIndex == 1) {
			#$BirthdaysOnlyCheckbox->set_sensitive(0);
			$ExportWindow->set_action('select-folder');
		} else {
			#$BirthdaysOnlyCheckbox->set_sensitive(1);
			$ExportWindow->set_action('save');
		}
	});
	$Export_Combo->set_active(0);

	while (1) {
		my $Response = $ExportWindow->run();
		my $Filename = $ExportWindow->get_filename();
		if($Response eq 'accept') {
			#my $OnlyBirthdays = $BirthdaysOnlyCheckbox->get_active();
			if ($ActiveIndex == 0) {
				if(PromptOverwrite("$Filename.ics")) {
					$iCalendar->write("$Filename.ics");
					last;
				}
			} elsif ($ActiveIndex == 1) {
				if(PromptOverwrite($Filename, 1)) {
					HTML_Export($Filename);
					last;
				}
			} else {
				DPIntWarn("Unknown activeindex: $ActiveIndex");
			}
		} else {
			last;
		}
	}
	$ExportWindow->destroy();
}

# Purpose: Prompt the user to overwrite data
# Usage: PromptOverwrite(PATH, DIR?);
#	Returns undef on "don't overwrite". Otherwise true.
sub PromptOverwrite {
	my ($File, $Dir) = @_;
	if($Dir) {
		if(-e $File) {
			if(-d $File) {
				while(glob("$File/*")) {
					unless($_ =~ /^\./) {
						if(DPQuestion($i18n->get_advanced("There are already files in the directory \"%(dir)\". If you continue any existing exported Day Planner data in that directory will be overwritten. Do you want to continue?", { dir =>  $File}))) {
							return(1);
						} else {
							return(undef);
						}
					}
				}
				return(1);
			} else {
				DPInfo($i18n->get_advanced("\"%(file)\" already exists and is not a directory", { file => $File }));
				return(undef);
			}
		} else {
			return(1);
		}
	} else {
		runtime_use('File::Basename');
		my $FileDirname = dirname($File);
		unless(-w $FileDirname) {
			DPInfo($i18n->get_advanced("\"%(dir)\" is read only. Write permissions are required.", { dir => $FileDirname}));
			return(undef);
		}
		if(-e $File) {
			if(-d $File) {
				DPInfo($i18n->get_advanced("\"%(dir)\" is a directory.", { dir => $File }));
				return(undef);
			} elsif(DPQuestion($i18n->get_advanced("\"%(file)\" already exists. Overwrite?", { file => $File}))) {
				return(1);
			} else {
				return(undef);
			}
		} else {
			return(1);
		}
	}
}

# =============================================================================
# GUI CODE
# =============================================================================

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# GUI helper functions
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Work around limitations in older Gtk2 versions that don't have
# 	->set_image on buttons
# Usage: Gtk2_Button_SetImage(BUTTON, IMAGE, LABEL);
# 	Sets LABEL if version is old
sub Gtk2_Button_SetImage {
	my($button,$image,$label) = @_;
	if($Gtk2::VERSION <= 1.070) {
		DPIntWarn("You're running Day Planner using an old version of Gtk2. This version contains certain limitations which will be worked around. This will cause the interface to look sub-optimal, you are encouraged to upgrade to a later version of Gtk2/Gtk2-perl");
		return $button->set_label($i18n->get($label));
	} else {
		return $button->set_image($image);
	}
}	

# Purpose: Initialize gtk2
# Usage: Gtk2Init();
sub Gtk2Init {
	Gtk2->init();
	$Gtk2Init = 1;
}

# Purpose: Create a progresswindow
# Usage: my $ProgressWin = DPCreateProgressWin(WINDOW NAME, INITIAL PROGRESS BAR TEXT, PULSATE_MODE?);
#	Returns a hashref with the following keys:
#		Window = The window
#		ProgressBar = The progress bar
sub DPCreateProgressWin {
	my ($Name, $Text, $PulsateMode) = @_;
	my %ProgressHash;
	$ProgressHash{Window} = Gtk2::Window->new();
	$ProgressHash{Window}->set_skip_taskbar_hint(1);
	$ProgressHash{Window}->set_skip_pager_hint(1);
	if(defined($Name)) {
		$ProgressHash{Window}->set_title($Name);
	}
	if(defined($MainWindow)) {
		$ProgressHash{Window}->set_transient_for($MainWindow);
		$ProgressHash{Window}->set_position('center-on-parent');
	} else {
		$ProgressHash{Window}->set_position('center');
	}
	$ProgressHash{ProgressBar} = Gtk2::ProgressBar->new();
	$ProgressHash{Window}->add($ProgressHash{ProgressBar});
	$ProgressHash{Window}->set_modal(1);
	$ProgressHash{Window}->set_resizable(0);
	if(defined($Text)) {
		$ProgressHash{ProgressBar}->set_text($Text);
	} else {
		$ProgressHash{ProgressBar}->set_fraction(0);
	}
	if($PulsateMode) {
		$ProgressHash{ProgressBar}->{activity_mode} = 0;
	}
	$ProgressHash{ProgressBar}->show();
	$ProgressHash{Window}->show();
	Gtk2->main_iteration while Gtk2->events_pending;
	Gtk2->main_iteration while Gtk2->events_pending;
	Gtk2->main_iteration while Gtk2->events_pending;
	return(\%ProgressHash);
}

# Purpose: Pulsate a progressbar
# Usage: PulsateProgressbar($Progressbar);
sub PulsateProgressbar {
	my $Progressbar = $_[0];
	if(defined($Progressbar)) {		# So that the calling function can just *assume* it has a progressbar
						# even when it doesn't
		$Progressbar->pulse();
		Gtk2->main_iteration while Gtk2->events_pending;
	}
	return(1);
}

# Purpose: Set the progress of a progressbar
# Usage: ProgressMade(FUNCTIONS TO PERFORM, FUNCTIONS PERFORMED, $ProgressWindowHashref, NewText?);
sub ProgressMade {
	return unless $Gtk2Init;

	my ($ToPerform, $Performed, $ProgressHash,$Text) = @_;
	my $Bar = $ProgressHash->{ProgressBar};
	my $Result = sprintf("%d", ($Performed / $ToPerform) * 100);
	if($Result < 100) {
		$Result = "0.$Result"; 
	} else {
		$Result = 1;
	}
	$Bar->set_fraction($Result);
	if($Text) {
		$Bar->set_text($Text);
	}
	Gtk2->main_iteration while Gtk2->events_pending;
	Gtk2->main_iteration while Gtk2->events_pending;
	Gtk2->main_iteration while Gtk2->events_pending;
	return(1, $Result);
}

# Purpose: Set the fraction in a progressbar
# Usage: ProgressFraction($Progressbar, fraction);
sub ProgressFraction {
	my($Progressbar, $fraction) = @_;
	if($Gtk2Init and defined($Progressbar)) {
		$Progressbar->set_fraction($fraction);
		Gtk2->main_iteration while Gtk2->events_pending
	}
}

# Purpose: Populate the upcoming events widget
# Usage: PopulateUpcomingEvents();
sub PopulateUpcomingEvents {
	my $NewUpcoming;
	my $HasUpcoming;
	my %InformationHash;
	my %DayNames = (
		0 => $i18n->get('Sunday'),
		1 => $i18n->get('Monday'),
		2 => $i18n->get('Tuesday'),
		3 => $i18n->get('Wednesday'),
		4 => $i18n->get('Thursday'),
		5 => $i18n->get('Friday'),
		6 => $i18n->get('Saturday'),
	);

	# Today is
	my $TheTime = time();

	# Prepare
	my $FirstDay = 1;
	my $AddDays = 7;

	# Loop used to populate the %InformationHash
	while($AddDays) {
		$AddDays--;	# One less day to add
		$TheTime += 86400;

		$InformationHash{$TheTime} = {};
		my $h = $InformationHash{$TheTime};
		
		my ($getsec,$getmin,$gethour,$getmday,$getmonth,$getyear,$getwday,$getyday,$getisdst) = localtime($TheTime);	# Get the real time of this day
		$getmonth++;	# Month should be 1-12 not 0-11
		my $Year = $getyear;

		my $HumanYear = $getyear+1900;	# Human readable year

		if($FirstDay) {
			$h->{text}= $i18n->get('Tomorrow');
			$h->{dayname} = $h->{text};
			$FirstDay = 0;
		} else {
			$h->{text} .= "\n\n";
			$h->{text} .= $DayNames{$getwday};
			$h->{dayname} = $DayNames{$getwday};
		}
		$h->{date} = "$getmday.$getmonth.$HumanYear";
		$h->{text} .= " ($getmday.$getmonth.$HumanYear) :";
		my $HasEvents;
		if(my $DateHash = $iCalendar->get_dateinfo($Year+1900, $getmonth, $getmday)) {
			# FIXME: This sort should be so that alphabetical chars come first, then numbers
			# This so that DAY comes before the normal events.
			foreach my $time (sort(@{$iCalendar->get_dateinfo($HumanYear,$getmonth,$getmday)})) {
				foreach my $UID (@{$iCalendar->get_timeinfo($HumanYear,$getmonth,$getmday,$time)}) {
					$HasEvents = 1;
					# If the time is DAY then it lasts the entire day
					if($time eq 'DAY') {
						$h->{text} .= "\n" . GetSummaryString($UID);
					} else {
						# TRANSLATORS: String used in the list of upcoming events in the
						# 		lower right hand corner of the UI. You should
						# 		probably keep this short.
						$h->{text} .= "\n" . $i18n->get_advanced('At %(time)', { 'time' =>  $i18n->AMPM_From24($time)}) . ': ' . GetSummaryString($UID);
					}
				}
			}
		}
		unless($HasEvents) {
			# TRANSLATORS: This is used in the upcoming events widget. It is displayed for a day (or set of days)
			#  when no events are present. Ie: Tomorrow (24.02.2007): (nothing).
			$h->{text} .= "\n" . $i18n->get('(nothing)');
			$h->{noevents} = 1;
		} else {
			$HasUpcoming = 1;
		}

	}
	unless($HasUpcoming) {
		$NewUpcoming = $i18n->get('No upcoming events exist for the next seven days');
	} else {
		my $LoopNum;
		# Remove duplicate (nothing)'s
		foreach my $key (sort(keys(%InformationHash))) {
			# If the key doesn't exist (any more) or doesn't have noevents then skip it
			next unless(defined($InformationHash{$key}));
			$LoopNum++;		# Up the loop counter
			next unless(defined($InformationHash{$key}{noevents}));
			# Find out which key is next
			my $Next = $key;
			$Next += 86400;
			# Skip if the next key doesn't have noevents set
			next unless(defined($InformationHash{$Next}) and defined($InformationHash{$Next}{noevents}));

			my @OtherNoevents;	# Array of the next keys without any events
			my $LastDate;
			my $LastDay;		# The last day with noevents

			# For each of the next dates with no events set it up for usage here and if we had another
			# noevents before this push it onto the @OtherNoevents array and replace the $LastDay value
			# with ours.
			while(defined($InformationHash{$Next}) and defined($InformationHash{$Next}{noevents})) {
				$LastDate = $InformationHash{$Next}{date};
				if(defined($LastDay)) {
					push(@OtherNoevents, $LastDay);
				}
				$LastDay = $Next;
				$Next += 86400;
			}
			# Reset the current text 
			if($LoopNum > 1) {
				$InformationHash{$key}{text} = "\n\n";
			} else {
				$InformationHash{$key}{text} = '';
			}
			# If there is something in @OtherNoevents then do more processing
			if(@OtherNoevents) {
				# First day (current key)
				$InformationHash{$key}{text} .= "$InformationHash{$key}{dayname},";

				my $Counter;	# Count how many times we've gone through the foreach
				foreach(@OtherNoevents) {
					$Counter++;	# Up the counter
					$InformationHash{$key}{text} .= " $InformationHash{$_}{dayname}";
					unless($Counter eq scalar(@OtherNoevents)) {	# If the counter doesn't equal the number of entries
											# in the array then append a comma.
						$InformationHash{$key}{text} .= ',';
					}
					# Delete the key
					delete($InformationHash{$_});
				}
				# Append the last entries
				# TRANSLATORS: This is used to bind together a list. It is a list of days
				# 	such as: Friday, Saturday *and* Sunday.
				$InformationHash{$key}{text} .= ' ' . $i18n->get('and') . " $InformationHash{$LastDay}{dayname}";
			} else {
				# Build the string
				$InformationHash{$key}{text} .= "$InformationHash{$key}{dayname} " . $i18n->get('and') . " $InformationHash{$LastDay}{dayname}";
			}
			# Delete the $LastDay key
			delete($InformationHash{$LastDay});
			# Finalize the string
			$InformationHash{$key}{text} .= " ($InformationHash{$key}{date}-$LastDate): " . $i18n->get('(nothing)');
		}
		# Build our $NewUpcoming
		foreach my $key(sort(keys(%InformationHash))) {
			$NewUpcoming .= $InformationHash{$key}{text};
		}
	}
	# Don't update the widget if the text hasn't changed
	if(not $UpcomingEventsWidget->get_buffer eq $NewUpcoming) {
		$UpcomingEventsBuffer->set_text($NewUpcoming);
	}
}

# Purpose: Detect the path to the image file(s) supplied. Returns the path to the
#		first one found or undef
# Usage: $Image = DetectImage(image1, image2);
sub DetectImage {
	my $I_Am_At = $FindBin::RealBin;
	foreach my $Image (@_) {
		foreach my $Dir ("$I_Am_At/art", $I_Am_At, '/usr/share/dayplanner', '/usr/local/dayplanner', '/usr/local/share/dayplanner', '/usr/share/dayplanner/art', '/usr/local/dayplanner/art', '/usr/local/share/dayplanner/art', '/usr/share/icons/large', '/usr/share/icons', '/usr/share/icons/mini') {
			if (-e "$Dir/$Image") {
				return("$Dir/$Image");
			}
		}
	}
	return(undef);
}

# Purpose: Delete the event currently selected in the eventlist
# Usage: DeleteEvent();
sub DeleteEvent {
	my $Selected = [$EventlistWidget->get_selected_indices]->[0];
	# Unless $Selected is defined we don't have anything selected in the eventlist
	unless(defined($Selected)) {
		DPIntWarn('DeletEvent() called without any event selected');
		return(0);
	}
	my $Type = GetEventListType();

	my @StandardTypes = ( 'normal', 'allday','bday' );

	if(grep($Type, @StandardTypes)) {
		my ($EventYear, $EventMonth, $EventDay) = $CalendarWidget->get_date();$EventMonth++;
		my $EventUID = $EventlistWidget->{data}[$Selected][0];
		my $EventTime = $EventlistWidget->{data}[$Selected][1];
		my $EventSummary = $EventlistWidget->{data}[$Selected][2];

		# This redraws the event list for us. This is faster
		# than doing the redrawing using the DrawEventlist function.
		my $Array = $EventlistWidget->{data};
		splice(@{$Array},$Selected,1);
		$EventlistWidget->{data} = $Array;
		$ToolbarEditButton->set_sensitive(0);
		$ToolbarDeleteButton->set_sensitive(0);
		$MenuEditEntry->set_sensitive(0);
		$MenuDeleteEntry->set_sensitive(0);
		Gtk2->main_iteration while Gtk2->events_pending;
		$iCalendar->delete($EventUID);

		UpdatedData(0,1);
	} elsif ($Type eq 'holiday') {
		DPInfo(sprintf($i18n->get("This is a predefined \"holiday\" event. This event can not be edited directly from within Day Planner. In order to change these events use a text editor and edit the file %s."), $HolidayFile));
	} else {
		DPIntWarn("DeleteEvent: Attempted to handle unsupported \$Type: $Type");
	}
}

# Purpose: Detect which kind of event is selected in the eventlist
# Usage: my $Type = GetEventListType();
sub GetEventListType {
	my $Selected = [$EventlistWidget->get_selected_indices]->[0];
	# If $Selected isn't defined then nothing *is* selected and thus we just return (false)
	unless(defined($Selected)) {
		return(0);
	}
	# Initialize
	my $Event;
	if(not $EventlistWidget->{data}[$Selected][0] eq 'NULL' and $iCalendar->exists($EventlistWidget->{data}[$Selected][0])) {
		my $Dummy = $EventlistWidget->{data}[$Selected][0];
		$Event = $iCalendar->get_info($EventlistWidget->{data}[$Selected][0]);
	}

	# Detect the event type
	if($Event) {
		if (defined($Event->{'X-DP-BIRTHDAY'}) and $Event->{'X-DP-BIRTHDAY'} eq 'TRUE') {
			return('bday');
		} elsif (not $Event->{DTSTART} =~ /\dT\d/) {
			# Assume all day since there is no T parameter
			return('allday');
		} elsif ($Event->{SUMMARY}) {
			# Assume normal since SUMMARY is set
			return('normal');
		} else {
			DPIntWarn("GetEventListType(): Unknown type of event $EventlistWidget->{data}[$Selected][0]. Dumping data.");
			print Dumper($Event);
		}
	} else {
		return('holiday');
	}
}

# Purpose: Close a window when the escape key is pressed
# Usage: $WIDGET->signal_connect("key_release_event" => \&EscapeKeyHandler);
sub EscapeKeyHandler {
	my ($widget, $event) = @_;
	if ($event->keyval == $Gtk2::Gdk::Keysyms{Escape}) {
		$widget->destroy();
	}
}

# Purpose: Display an error dialog
# Usage: DPError("Error message");
sub DPError {
	if($Gtk2Init) {
		my $Dialog = Gtk2::MessageDialog->new($MainWindow, 'modal', 'error', 'ok', $_[0]);
		$Dialog->set_title($i18n->get('Day Planner'));
		$Dialog->run();
		$Dialog->destroy();
	} else {
		warn($_[0]);
	}
	return(1);
}

# Purpose: Display a warning dialog
# Usage: DPWarning("Warning");
sub DPWarning {
	if($Gtk2Init) {
		my $Dialog = Gtk2::MessageDialog->new_with_markup($MainWindow, 'modal', 'warning', 'ok', $_[0]);
		$Dialog->set_title($i18n->get('Day Planner'));
		$Dialog->run();
		$Dialog->destroy();
	} else {
		warn($_[0]);
	}
	return(1);
}

# Purpose: Display an information dialog (with optional details)
# Usage: DPInfo("Information message", details?);
sub DPInfo {
	if($Gtk2Init) {
		my $Dialog = Gtk2::MessageDialog->new($MainWindow, 'modal', 'info', 'ok', $_[0]);
		if($_[1]) {
			# The expander
			my $FT_Expander = CreateDetailsWidget();
			$Dialog->vbox->add($FT_Expander);
			# The textview field
			my $FulltextView = Gtk2::TextView->new();
			$FulltextView->set_editable(0);
			$FulltextView->set_wrap_mode('word-char');
			$FulltextView->show();
			# Add the text to it
			my $FulltextBuffer = Gtk2::TextBuffer->new();
			$FulltextBuffer->set_text($_[1]);
			$FulltextView->set_buffer($FulltextBuffer);
			# Create a scrollable window to use
			my $FulltextWindow = Gtk2::ScrolledWindow->new;
			$FulltextWindow->set_policy('automatic', 'automatic');
			$FulltextWindow->add($FulltextView);
			$FulltextWindow->show();
			# Add it to the expander
			$FT_Expander->add($FulltextWindow);
		}
		$Dialog->set_title($i18n->get('Day Planner'));
		$Dialog->run();
		$Dialog->destroy();
	} else {
		print "$_[0]\n";
		if(defined($_[1])) {
			print "$_[1]\n";
		}
	}
}

# Purpose: Display a question dialog
# Usage: DPQuestion("Question");
#	Returns true on yes, false on anything else
sub DPQuestion {
	my $Dialog = Gtk2::MessageDialog->new(undef, 'modal', 'question', 'yes-no', $_[0]);
	$Dialog->set_title($i18n->get('Day Planner'));
	my $Reply = $Dialog->run();
	$Dialog->destroy();
	if ($Reply eq 'yes') {
		return(1);
	} else {
		return(0);
	}
}

# Purpose: Call save functions on exit
# Usage: QuitSub();
sub QuitSub {
	$MainWindow->set_sensitive(0);
	DPS_Perform('SYNC');
	my $SaveData = SaveMainData();
	if ($SaveData eq 'SAVE_FAILED') {
		unless(DPQuestion($i18n->get('Some files could not be saved correctly. Quit anyway?'))) {
			$MainWindow->show();
			return(1);
		}
	}
	WriteStateFile($SaveToDir, 'state.conf');
	
	Gtk2->main_quit;
	CloseIPC();
	exit(0);
}

# Purpose: Report a bug
# Usage: ReportBug();
sub ReportBug
{
	my $BugUrl = 'http://www.day-planner.org/index.php/development/bugs/?b_version='.$Version;
	if ($VersionName eq 'SVN')
	{
		$BugUrl .= '&b_issvn=1';
	}
	else
	{
		$BugUrl .= '&b_issvn=0';
	}
	LaunchWebBrowser($BugUrl);
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# GUI calendar functions
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Sets the active calendar items in the $CalendarWidget
# Usage: SetActiveCalItems(YEAR, NUMERICAL_MONTH[1-12]);
sub SetActiveCalItems {
	$CalendarWidget->clear_marks;			# Clear the current marks
	# Calendar contents
	my $MonthInfo = $iCalendar->get_monthinfo(@_);
	if ($MonthInfo) {
		foreach my $Day (@{$MonthInfo}) {
			$CalendarWidget->mark_day($Day);	# Mark this day
		}
	}
}

# 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.
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: Calls SetActiveCalItems on the current year/month displayed in the $CalendarWidget
# Usage: CalendarChange();
sub CalendarChange {
	my $Month = $CalendarWidget->month;
	$Month++;
	SetActiveCalItems($CalendarWidget->year, $Month);
}

# Purpose: Get the day, month and year in a single string from a calendar
# Usage: my $Date = Get_DateInfo($CALWIDGET);
sub Get_DateInfo {
	my ($year, $month, $day) = $CalendarWidget->get_date();
	return("$year$month$day");
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# GUI event adding and editing functions
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# HELPER FUNCTIONS

# Purpose: Create the details widget
# Usage: my $ExpanderWidget = CreateDetailsWidget();
sub CreateDetailsWidget {
	my $FT_Expander = Gtk2::Expander->new($i18n->get('Show advanced settings'));
	$FT_Expander->show();
	$FT_Expander->signal_connect('activate' => sub {
			# Yes, it is weird to use not here, but in the callback it appears
			# to return "" if it is expanded and 1 if it isn't.
			# Possibly a race condition within gtk2. This appears to work anyway.
			if(not $FT_Expander->get_expanded) {
				$FT_Expander->set_label($i18n->get('Hide advanced settings'));
			} else {
				$FT_Expander->set_label($i18n->get('Show advanced settings'));
			}
		});
	return($FT_Expander);
}

# Purpose: Parse the contents of a text entry field containing various dates
# 		and return an arrayref of dates in the format DD.MM.YYYY
# Usage: my $Ref = ParseEntryField(TEXT ENTRY OBJECT);
sub ParseEntryField {
	my $Field = shift;
	# First get the text
	my $FieldText = $Field->get_text();
	# If it is empty then return an empty array
	if(not $FieldText =~ /\S/) {
		return([]);
	}
	my @ReturnArray;
	# Parse the entry field
	foreach my $Text (split(/[,|\s]+/, $FieldText)) {
		$Text =~ s/\s+//g;
		if($Text =~ /^\d+\.\d+.\d\d\d\d$/) {
			push(@ReturnArray, $Text);
		} else {
			DPIntWarn("Unrecognized date string (should be DD.MM.YYYY): $Text");
		}
	}
	return(\@ReturnArray);
}

# Purpose: Parse a date string and return various date fields
# Usage: my ($Year, $Month, $Day) = ParseDateString(STRING);
sub ParseDateString {
	my $String = shift;
	# This function is currently stupid, so it doesn't really support more than
	# one format. It is also very strict about that format.
	# This can easily be improved though.
	my $Year = $String;
	my $Month = $String;
	my $Day = $String;
	$Year =~ s/^\d+\.\d+\.(\d\d\d\d)$/$1/;
	$Month =~ s/^\d+\.(\d+)\.\d\d\d\d$/$1/;
	$Day =~ s/^(\d+).*$/$1/;

	# Drop leading zeros from returning
	$Month =~ s/^0//;
	$Day =~ s/^0//;
	return($Year,$Month,$Day);
}

# Purpose: Add a date to a text field
# Usage: AddToField(ENTRY, YEAR, MONTH, DAY);
sub AddToField {
	my($Entry,$Year,$Month,$Day) = @_;

	$Month = AppendZero($Month);
	$Day = AppendZero($Day);
	# First get the text
	my $FieldText = $Entry->get_text();
	# Then add to the text
	if($FieldText =~ /\S/) {
		$FieldText .= ", ";
	}
	$FieldText .= "$Day.$Month.$Year";
	$Entry->set_text($FieldText);
}

# Purpose: Remove a date from a text field (more tricky)
# Usage: RemoveFromField(ENTRY, YEAR, MONTH, DAY);
sub RemoveFromField {
	my($Entry,$EventYear,$EventMonth,$EventDay) = @_;
	# First get the text
	my $FieldText = $Entry->get_text();
	# Then redo it
	my $NewField = "";
	foreach my $String (split(/[,|\s]+/, $FieldText)) {
		my $Year = $String;
		my $Month = $String;
		my $Day = $String;
		$Year =~ s/^\d+\.\d+\.(\d\d\d\d)$/$1/;
		$Month =~ s/^\d+\.(\d+)\.\d\d\d\d$/$1/;
		$Day =~ s/^(\d+).*$/$1/;
		$Month =~ s/^0//;
		$Day =~ s/^0//;
		if(not $Year eq $EventYear or not $Month eq $EventMonth or not $Day eq $EventDay) {
			if($NewField) {
				$NewField .= ", ";
			}
			$NewField .= "$Day.$Month.$Year"
		}
	}
	$Entry->set_text($NewField);
}

# Purpose: Popup a calendar to select one or more dates from
# Usage: \&PopupDateSel(TEXT ENTRY OBJECT, CALENDAR BUTTON, MODE, DEFAULT_DATE?);
# 	MODE is one of:
#	 	SINGLE - Only allows a single date to be selected
# 		MULTI/undef - Allows an infinite number of dates to be selected
# 	DEFAULT_DATE is one of:
# 		undef - Use the first entry in the entry field
# 		CURRENT - Use todays date
sub PopupDateSel {
	my $TextEntry = shift;
	my $CalButton = shift;
	$CalButton->set_sensitive(0);
	my $Mode = shift;
	my $Default_Date = shift;
	
	# This is used to indicate when we are changing months,
	# so that changing month doesn't inadvertedly change the date too.
	my $ChangingNow;

	if(not $Mode) {
		$Mode = 'MULTI';
	}

	# Create the window and stuff inside the window
	my $Window = Gtk2::Window->new();
	$Window->set_deletable(0);
	$Window->set_modal(1);
	$Window->set_skip_taskbar_hint(1);
	$Window->set_skip_pager_hint(1);
	$Window->set_title($i18n->get('Calendar'));
	$Window->set_position('mouse');
	$Window->set_resizable(0);
	$Window->set_border_width(5);
	$Window->set_transient_for($MainWindow);
	$Window->set_type_hint('dialog');
	$Window->signal_connect('destroy' => sub {
		$Window->destroy();
			$CalButton->set_sensitive(1);
			$TextEntry->grab_focus();
		});
	$Window->signal_connect('delete-event' => sub {
		$Window->destroy();
			$CalButton->set_sensitive(1);
			$TextEntry->grab_focus();
		});
	my $VBox = Gtk2::VBox->new();
	$Window->add($VBox);
	my $Calendar = Gtk2::Calendar->new();
	$VBox->pack_start($Calendar,0,0,0);
	my $ButHBox = Gtk2::HBox->new();
	$VBox->pack_end($ButHBox,0,0,0);
	my $CloseButton = Gtk2::Button->new_from_stock('gtk-close');
	$CloseButton->signal_connect(clicked => sub {
			$Window->destroy();
			$CalButton->set_sensitive(1);
			$TextEntry->grab_focus();
		});
	$ButHBox->pack_end($CloseButton,0,0,0);
	# Handle changes to the calendar
	my %SelHash;
	# Select the date in the entry field
	if(defined($Default_Date) and $Default_Date eq 'CURRENT') {
		my ($year, $month, $day) = $CalendarWidget->get_date();
		$Calendar->select_month($month, $year);
	} else {
		my $CurrDate = ParseEntryField($TextEntry);
		if(@{$CurrDate}) {
			my($SelYear,$SelMonth,$SelDay) = ParseDateString($CurrDate->[0]);
			$SelMonth--;
			$Calendar->select_month($SelMonth,$SelYear);
			$Calendar->select_day($SelDay);
		}
	}

		# Get the dates
	$Calendar->signal_connect('month-changed' => sub {
			# If ChangingNow is undef that means we just
			# got created, so initialize it to zero instead of one
			# so that selecting days work properly immedietly.
			if(not defined $ChangingNow) {
				$ChangingNow = 0;
			} else {
				$ChangingNow = 1;
			}
			my $DateRef = ParseEntryField($TextEntry);
			my ($CurrYear, $CurrMonth, $CurrDay) = $Calendar->get_date();
			$CurrMonth++;	# 1-12 not 0-11
			%SelHash = ();
			$Calendar->clear_marks();
			foreach my $Date (@{$DateRef}) {
				my($Year,$Month,$Day) = ParseDateString($Date);
				if($Year eq $CurrYear and $Month eq $CurrMonth) {
					$SelHash{$Day} = 1;
					$Calendar->mark_day($Day);
				}
			}
	});
	$Calendar->signal_emit('month-changed');
		# Set the handler for clicking on a date
		# This will add and remove dates from the field
	$Calendar->signal_connect('day-selected' => sub {
			if($ChangingNow) {
				$ChangingNow = 0;
				return;
			}
			my $Selected = $Calendar->selected_day();
			my ($CurrYear, $CurrMonth, $CurrDay) = $Calendar->get_date();
			$CurrMonth++;	# Month is 0-11 not 1-12. We want 1-12
			if($SelHash{$Selected}) {
				if(not $Mode eq 'SINGLE') {
					$Calendar->unmark_day($Selected);
					delete($SelHash{$Selected});
					RemoveFromField($TextEntry,$CurrYear,$CurrMonth,$CurrDay);
				}
			} else {
				if($Mode eq 'SINGLE') {
					$Calendar->clear_marks();
					%SelHash = ();
					$TextEntry->set_text('');
				}
				$Calendar->mark_day($Selected);
				$SelHash{$Selected} = 1;
				AddToField($TextEntry,$CurrYear,$CurrMonth,$CurrDay);
			}
			});
	$Calendar->signal_connect('day-selected-double-click' => sub {
			if($Mode eq 'SINGLE') {
				$Calendar->signal_emit('day-selected');
				$CloseButton->signal_emit('clicked');
			}
		});

	# Show it all
	$Window->show_all();
}

# Purpose: Create the advanced settings widget
# Usage: my $ExpanderWidget = CreateAdvancedWidget(RRULE?,EXDATE?);
# 	RRULE is an RRULE hash as returned by DP::iCalendars ->get_RRULE.
# 	It can be undef.
sub CreateAdvancedWidget {
	my $RRULE = shift;
	my $EXDATE = shift;
	my $UNTIL = shift;
	# Create the expander and set up signal handlers
	my $FT_Expander = Gtk2::Expander->new($i18n->get('Show advanced settings'));
	$FT_Expander->show();
	$FT_Expander->signal_connect('activate' => sub {
			# Yes, it is weird to use not here, but in the callback it appears
			# to return "" if it is expanded and 1 if it isn't.
			# Possibly a race condition within gtk2. This appears to work anyway.
			if(not $FT_Expander->get_expanded) {
				$FT_Expander->set_label($i18n->get('Hide advanced settings'));
			} else {
				$FT_Expander->set_label($i18n->get('Show advanced settings'));
			}
		});
	# Create the vbox to use within the expander
	my $ExpanderVBox = Gtk2::VBox->new();
	$ExpanderVBox->show();
	$FT_Expander->add($ExpanderVBox);
	# Create the recurrance checkbutton
		# HBox for it
	my $RecurHBox = Gtk2::HBox->new();
	$RecurHBox->show();
	$ExpanderVBox->pack_start($RecurHBox,0,0,0);
		# Checkbutton for it
	my $RecurCheckButton = Gtk2::CheckButton->new($i18n->get('Repeat event every:') . ' ');
	$RecurCheckButton->show();
	$RecurHBox->pack_start($RecurCheckButton,0,0,0);
	my $Table;		# For use later, needs to be declared here to be used in the RecurCheckButton callback
		# The combo for it
	my $Recur_Combo = Gtk2::ComboBox->new_text();
	# TRANSLATORS: Context: Repeat event every: THIS STRING
	$Recur_Combo->insert_text(0, $i18n->get('day'));
	# TRANSLATORS: Context: Repeat event every: THIS STRING
	$Recur_Combo->insert_text(1, $i18n->get('week'));
	# TRANSLATORS: Context: Repeat event every: THIS STRING
	$Recur_Combo->insert_text(2, $i18n->get('month'));
	# TRANSLATORS: Context: Repeat event every: THIS STRING
	$Recur_Combo->insert_text(3, $i18n->get('year'));
	$Recur_Combo->set_active(0);
	$Recur_Combo->show();
	$Recur_Combo->set_sensitive(0);
		# Checkbutton callback
	$RecurCheckButton->signal_connect('toggled' => sub {
			if($RecurCheckButton->get_active) {
				$Recur_Combo->set_sensitive(1);
				$Table->set_sensitive(1);
			} else {
				$Recur_Combo->set_sensitive(0);
				$Table->set_sensitive(0);
			}
		});
	$RecurHBox->pack_start($Recur_Combo,0,0,0);
		# Recurrance Table
	$Table = Gtk2::Table->new(2,4);
	$Table->set_sensitive(0);
	$Table->show();
	# Create the recurrance ends checkbutton
		# This is exceedingly ugly. But...works
	my $SecondIndentLabel = Gtk2::Label->new('    ');
	$SecondIndentLabel->show();
	$Table->attach_defaults($SecondIndentLabel,0,1,0,1);
		# Checkbutton for it
		# TRANSLATORS: The label in front of the entry box where you enter the date when a recurring event should stop recurring.
		my $StopAtCheckButton = Gtk2::CheckButton->new($i18n->get('Until:') . ' ');
	$StopAtCheckButton->show();
	$Table->attach_defaults($StopAtCheckButton,1,2,0,1);
		# The entry field
	my $StopAt_Entry = Gtk2::Entry->new();
	$StopAt_Entry->show();
	$StopAt_Entry->set_sensitive(0);
	$Table->attach_defaults($StopAt_Entry,2,3,0,1);
		# The calendar button
	my $StopAt_CalButton = Gtk2::Button->new();
	my $SecondCalImage = Gtk2::Image->new_from_file(DetectImage('x-office-calendar.png','dayplanner-16x16.png'));
	Gtk2_Button_SetImage($StopAt_CalButton,$SecondCalImage,'Calendar');
	$StopAt_CalButton->show();
	$StopAt_CalButton->set_sensitive(0);
	$StopAt_CalButton->signal_connect('clicked' => sub { PopupDateSel($StopAt_Entry,$StopAt_CalButton,'SINGLE','CURRENT')});
	$Table->attach_defaults($StopAt_CalButton,3,4,0,1);
		# CheckButton callback
	$StopAtCheckButton->signal_connect('toggled' => sub {
			if($StopAtCheckButton->get_active) {
				$StopAt_Entry->set_sensitive(1);
				$StopAt_CalButton->set_sensitive(1);
			} else {
				$StopAt_Entry->set_sensitive(0);
				$StopAt_CalButton->set_sensitive(0);
			}
		});
	# Create the recurrance exception checkbutton
		# HBox for it
	$ExpanderVBox->pack_start($Table,0,0,0);
		# This is exceedingly ugly. But...works
	my $IndentLabel = Gtk2::Label->new('    ');
	$IndentLabel->show();
	$Table->attach_defaults($IndentLabel,0,1,1,2);
		# Checkbutton for it
		# TRANSLATORS: Don't recur the event on the dates specified next to this string
	my $ExceptCheckButton = Gtk2::CheckButton->new($i18n->get('But not on:') . ' ');
	$ExceptCheckButton->show();
	$Table->attach_defaults($ExceptCheckButton,1,2,1,2);
		# The entry field
	my $ExceptEntry = Gtk2::Entry->new();
	$ExceptEntry->show();
	$ExceptEntry->set_sensitive(0);
	$Table->attach_defaults($ExceptEntry,2,3,1,2);
		# The calendar button
	my $CalButton = Gtk2::Button->new();
	my $CalImage = Gtk2::Image->new_from_file(DetectImage('x-office-calendar.png','dayplanner-16x16.png'));
	Gtk2_Button_SetImage($CalButton,$CalImage,'Calendar');
	$CalButton->show();
	$CalButton->set_sensitive(0);
	$CalButton->signal_connect('clicked' => sub { PopupDateSel($ExceptEntry,$CalButton,'MULTI','CURRENT')});
	$Table->attach_defaults($CalButton,3,4,1,2);
		# CheckButton callback
	$ExceptCheckButton->signal_connect('toggled' => sub {
			if($ExceptCheckButton->get_active) {
				$ExceptEntry->set_sensitive(1);
				$CalButton->set_sensitive(1);
			} else {
				$ExceptEntry->set_sensitive(0);
				$CalButton->set_sensitive(0);
			}
		});
		
		# Get RRULE information if provided and set it
	if($RRULE) {
		$FT_Expander->signal_emit('activate');
		my $FREQ = $RRULE->{FREQ};
		$RecurCheckButton->set_active(1);
		$Table->set_sensitive(1);
		if($FREQ eq 'DAILY') {
			$Recur_Combo->set_active(0);
		} elsif($FREQ eq 'WEEKLY') {
			$Recur_Combo->set_active(1);
		} elsif($FREQ eq 'MONTHLY') {
			$Recur_Combo->set_active(2);
		} elsif($FREQ eq 'YEARLY') {
			$Recur_Combo->set_active(3);
		}
		# Get UNTIL information if provided and set it
		if($RRULE->{UNTIL}) {
			$StopAtCheckButton->set_active(1);
			AddToField($StopAt_Entry,iCal_ParseDateTime($RRULE->{UNTIL}));
		}
		# Get EXDATE information if provided and set it
		if(@{$EXDATE}) {
			$ExceptCheckButton->set_active(1);
			foreach my $entry (sort @{$EXDATE}) {
				AddToField($ExceptEntry,iCal_ParseDateTime($entry));
			}
		}
	}

	# Create the entry for the fulltext.
	# The caller should add text and such as needed
	my $FulltextEntry = Gtk2::TextView->new();
	$FulltextEntry->set_editable(1);
	$FulltextEntry->set_wrap_mode('word-char');
	$FulltextEntry->show();
	$FulltextEntry->set_accepts_tab(0);
	my $FulltextWindow = Gtk2::ScrolledWindow->new;
	$FulltextWindow->set_policy('automatic', 'automatic');
	$FulltextWindow->add($FulltextEntry);
	$FulltextWindow->show();
	my $FTFrame = Gtk2::Frame->new();
	$FTFrame->show();
	$FTFrame->add($FulltextWindow);
	$FTFrame->set_label($i18n->get('Detailed description'));
	$ExpanderVBox->pack_start($FTFrame,0,0,0);
	return($FT_Expander,$FulltextEntry,$Recur_Combo,$ExceptEntry,$StopAt_Entry);
}

# Purpose: Get a properly formatted event time from two widgets (min/hour)
# Usage: my $Time = GetTimeFromWidgets($HourSpinner, $MinuteSpinner, $AMPM);
sub GetTimeFromWidgets {
	# In the future this sub will take a third argument, which is either an AM/PM
	# selection box, or undef - so that seamless AM/PM support is needed.
	
	my ($HourSpinner, $MinuteSpinner, $AMPM) = @_;	
	my $Hour = $HourSpinner->get_value_as_int();
	my $Minute = $MinuteSpinner->get_value_as_int();
	if ($Hour <= 9) {
		$Hour = "0$Hour";
	}
	if ($Minute <= 9) {
		$Minute = "0$Minute";
	}
	if(defined($AMPM)) {
		my $Prefix;
		if($AMPM->get_active() == 0) {
			$Prefix = $i18n->get_ampmstring('AM');
		} else {
			$Prefix = $i18n->get_ampmstring('PM');
		}
		($Hour,$Minute) = $i18n->AMPM_To24("$Hour:$Minute $Prefix", 1);
	}
	return("$Hour:$Minute");
}

# Purpose: Get a formatted RRULE from the combo box.
# Usage: my $RRULE = GetRRULEFromCombo(WIDGET_HASH);
sub GetRRULEFromCombo {
	my $Hash = shift;
	my $combo = $Hash->{RecurCombo};
	if(not $combo->is_sensitive) {
		return(undef);
	}
	my $RRULE;
	# 0 == day
	# 1 == week
	# 2 == month
	# 3 == year
	my $ActiveIndex = $combo->get_active;
	if($ActiveIndex == 0) {
		$RRULE = 'FREQ=DAILY';
	} elsif ($ActiveIndex == 1) {
		$RRULE = 'FREQ=WEEKLY';
	} elsif ($ActiveIndex == 2) {
		$RRULE = 'FREQ=MONTHLY';
	} elsif ($ActiveIndex == 3) {
		$RRULE = 'FREQ=YEARLY;INTERVAL=1';
	}
	# Get UNTIL
	my $Until = GetDateFromEntry($Hash->{StopAt_Entry});
	if($Until) {
		$RRULE .= ";UNTIL=$Until";
	}
	return($RRULE);
}

# Purpose: Get a list of formatted EXDATE entries from the exception entry box
# Usage: my $List = GetExceptionsFromEntry(ENTRY WIDGET);
sub GetExceptionsFromEntry {
	my $Entry = shift;
	if (not $Entry->is_sensitive) {
		return([]);
	}
	my $EntryArray = ParseEntryField($Entry);
	my @ReturnArray;
	foreach my $Date (@{$EntryArray}) {
		my($Year,$Month,$Day) = ParseDateString($Date);
		$Month = AppendZero($Month);
		$Day = AppendZero($Day);
		push(@ReturnArray,"$Year$Month$Day");
	}
	return(\@ReturnArray);
}

# Purpose: Get the until entry from the until entry box
# Usage: my $UntilEntry = GetDateFromEntry(ENTRY WIDGET);
sub GetDateFromEntry {
	my $Entry = shift;
	if (not $Entry->is_sensitive) {
		return(undef);
	}
	my $EntryArray = ParseEntryField($Entry);
	my @ReturnArray;
	foreach my $Date (@{$EntryArray}) {
		my($Year,$Month,$Day) = ParseDateString($Date);
		$Month = AppendZero($Month);
		$Day = AppendZero($Day);
		if(wantarray()) {
			return($Year,$Month,$Day);
		} else {
			return("$Year$Month$Day");
		}
	}
	return(undef);
}

# Purpose: Create the widgets for selecting the time
# Usage: my ($HourSpinner, $MinuteSpinner, $TimeHBox, $AMPM) = TimeSelection("HH:MM");
sub TimeSelection {
	my $Time = shift;
	my($HourAdjustment,$MinuteAdjustment, $HourSpinner,$MinuteSpinner,$AMPM);

	if($i18n->get_clocktype() == 24) {
		# The hour adjustment
		$HourAdjustment = Gtk2::Adjustment->new(0.0, 0.0, 23.0, 1.0, 5.0, 0.0);
	} else {
		# The hour adjustment
		$HourAdjustment = Gtk2::Adjustment->new(0.0, 1.0, 12.0, 1.0, 5.0, 0.0);
	}
	# The minute adjustment
	$MinuteAdjustment = Gtk2::Adjustment->new(0.0, 0.0, 59.0, 1.0, 5.0, 0.0);
	# Create the spinners
	$HourSpinner = Gtk2::SpinButton->new($HourAdjustment, 0, 0);
	$MinuteSpinner = Gtk2::SpinButton->new($MinuteAdjustment, 0, 0);
	# Show them
	$HourSpinner->show();
	$MinuteSpinner->show();
	
	# Create a simple seperating label
	my $TimeSeperatorLabel = Gtk2::Label->new(' : ');
	$TimeSeperatorLabel->show();
	
	# Make them activate the default action
	$HourSpinner->set_activates_default(1);
	$MinuteSpinner->set_activates_default(1);
	
	if($i18n->get_clocktype() == 24) {
		# Get the time
		my $HSTime = $Time;		# Hour
		my $MSTime = $Time;		# Minute
		$HSTime =~ s/^(\d+):\d+$/$1/;
		$MSTime =~ s/^\d+:(\d+)$/$1/;
		# Set the initial value
		$HourSpinner->set_value($HSTime);
		$MinuteSpinner->set_value($MSTime);
	} else {
		# Get the time
		my $HSTime = $i18n->AMPM_From24($Time);
		my $MSTime = $i18n->AMPM_From24($Time);
		my $Suffix = $i18n->AMPM_From24($Time);
		$HSTime =~ s/^(\d+):.*$/$1/;
		$MSTime =~ s/^\d+:(\d+).*$/$1/;
		$Suffix =~ s/^\d+:\d+\s+(.+)/$1/;
		# Set the initial value
		$HourSpinner->set_value($HSTime);
		$MinuteSpinner->set_value($MSTime);
		# Create the combo box
		$AMPM = Gtk2::ComboBox->new_text;
		$AMPM->insert_text(0, $i18n->get_ampmstring('AM'));
		$AMPM->insert_text(1, $i18n->get_ampmstring('PM'));
		$AMPM->show();
		# Set the initial value
		if($Suffix eq $i18n->get_ampmstring('AM')) {
			$AMPM->set_active(0);
		} else {
			$AMPM->set_active(1);
		}
	}
	
	# Create a HBox and pack them onto it
	my $TimeSpinnerHBox = Gtk2::HBox->new(0,0);
	$TimeSpinnerHBox->pack_start($HourSpinner,0,0,0);
	$TimeSpinnerHBox->pack_start($TimeSeperatorLabel,0,0,0);
	$TimeSpinnerHBox->pack_start($MinuteSpinner,0,0,0);
	if(defined($AMPM)) {
		$TimeSpinnerHBox->pack_start($AMPM,0,0,0);
	}
	$TimeSpinnerHBox->show();

	# Return the widgets
	return($HourSpinner, $MinuteSpinner, $TimeSpinnerHBox, $AMPM);
}

# Purpose: Create the widgets for selecting the date
# Usage: my ($DateTable, $DateEntry) = DateSelection(UID?);
sub DateSelection {
	my $UID = shift;

	my $Table = Gtk2::Table->new(2,1);
	$Table->show();
		# The entry field
	my $Entry = Gtk2::Entry->new();
	$Entry->show();
	$Table->attach_defaults($Entry,0,1,0,1);
		# The calendar button
	my $CalButton = Gtk2::Button->new();
	my $CalImage = Gtk2::Image->new_from_file(DetectImage('x-office-calendar.png','dayplanner-16x16.png'));
	Gtk2_Button_SetImage($CalButton,$CalImage,'Calendar');
	$CalButton->show();
	$CalButton->signal_connect('clicked' => sub { PopupDateSel($Entry,$CalButton,'SINGLE')});
	$Table->attach_defaults($CalButton,1,2,0,1);
	
	# If we have an UID then get it from that, if not then get it from the calendar widget
	if(defined($UID) and not $UID eq 'NULL') {
		my $UIDObj = $iCalendar->get_info($UID);
		# Get date information from the UID
		AddToField($Entry,iCal_ParseDateTime($UIDObj->{DTSTART}))
	} else {
		my ($EventYear, $EventMonth, $EventDay) = $CalendarWidget->get_date();$EventMonth++;
		AddToField($Entry,$EventYear,$EventMonth,$EventDay);
	}
	
	return($Table,$Entry);
}

# Purpose: Create the window that will contain the event editor
# Usage: my ($Window, $VBox_HBoxContainer, $OKButton, $CancelButton) = CreateEventContainerWin(TITLE, OK_BUTTON_TYPE, UID);
#
# TITLE is the title of the window.
# OK_BUTTON_TYPE is the Gtk2::Stock ID to use for the button
# UID is the UID of the event. This can be undef.
sub CreateEventContainerWin {
	# Get the options passed to the sub
	my ($WindowTitle, $OkButtonType, $UID) = @_;
	
	# Get the date
	my ($EventYear, $EventMonth, $EventDay);
	# If we have an UID then get it from that, if not then get it from the calendar widget
	if($UID) {
		my $UIDObj = $iCalendar->get_info($UID);
		# Get date information from the UID
		($EventYear, $EventMonth, $EventDay) = iCal_ParseDateTime($UIDObj->{DTSTART});
		$EventMonth =~ s/^0//;	# Drop leading zero from event month.
	} else {
		($EventYear, $EventMonth, $EventDay) = $CalendarWidget->get_date();$EventMonth++;
	}

	# ==================================================================
	# BUILD THE WINDOW
	# ==================================================================
	my $AddEventBox = Gtk2::Window->new();
	$AddEventBox->set_modal(1);
	$AddEventBox->set_transient_for($MainWindow);
	$AddEventBox->set_position('center-on-parent');
	$AddEventBox->set_title($WindowTitle);
	$AddEventBox->set_resizable(0);
	$AddEventBox->set_border_width(5);
	$AddEventBox->set_skip_taskbar_hint(1);
	$AddEventBox->set_skip_pager_hint(1);
	$AddEventBox->set_type_hint('dialog');
	# This doesn't work. According to the people in #gtk-perl this is due to Gtk2's lack
	# of width-for-height. But might be implemented in later releases of gtk2.
	# For now I leave it commented out. It looks fine for English but doesn't look good
	# for languages using longer strings.
	#$AddEventBox->set_size_request(300,-1);
	# This doesn't work either. It "works" if the window is resizable. But then when the
	# expander is closed it looks like crap. It has no effect if the window isn't resizeable.
	#$AddEventBox->set_default_size(300,-1);

	# Handle closing
	$AddEventBox->signal_connect('destroy' => sub { $AddEventBox->destroy; $MainWindow->set_sensitive(1) });
	$AddEventBox->signal_connect('delete-event' => sub { $AddEventBox->destroy; $MainWindow->set_sensitive(1) });
	
	# Primary vbox
	my $ADPrimVBox = Gtk2::VBox->new();
	$AddEventBox->add($ADPrimVBox);
	$ADPrimVBox->show();
	
	# ==================================================================
	# Call the functions that constructs the main window contents
	# ==================================================================
	
	# Create the hbox for the buttons
	my $TB_ButtonHBox = Gtk2::HBox->new(0,6);
	$TB_ButtonHBox->show();
	$ADPrimVBox->pack_end($TB_ButtonHBox,0,0,0);
	
	# Create the OK button
	my $OKButton = Gtk2::Button->new_from_stock($OkButtonType);
	$OKButton->show();
	$TB_ButtonHBox->pack_end($OKButton,0,0,0);
	$OKButton->can_default(1);

	# Create the cancel button
	my $CancelButton = Gtk2::Button->new_from_stock('gtk-cancel');
	$CancelButton->signal_connect('clicked' => sub { $AddEventBox->destroy;
		});
	$CancelButton->show();
	$TB_ButtonHBox->pack_end($CancelButton,0,0,0);
	
	# Handle the esc button
	$AddEventBox->signal_connect('key_release_event' => \&EscapeKeyHandler);
	$AddEventBox->set_default($OKButton);

	# Return the widgets
	return($AddEventBox, $ADPrimVBox, $OKButton, $CancelButton);
}

# MAIN FUNCTIONS

# Purpose: Add a event. Creates the main window and calls the proper event functions
# Usage: AddEvent();
sub AddEvent {
	$MainWindow->set_sensitive(0);
        my ($EventYear, $EventMonth, $EventDay) = $CalendarWidget->get_date();$EventMonth++;
	my $OKSignal;
	my %TimeHash = (
		EventYear => $EventYear,
		EventMonth => $EventMonth,
		EventDay => $EventDay,
	);
		
	# Create the main window with the appropriate OK/Cancel buttons and the event frame
	my ($Window, $VBox_HBoxContainer, $OKButton, $CancelButton) = CreateEventContainerWin($i18n->get('Add an Event'), 'gtk-add');
	# HBox
	my $SelectorHBox = Gtk2::HBox->new();
	$VBox_HBoxContainer->pack_start($SelectorHBox,0,0,0);
	$SelectorHBox->show();
	# Label
	my $EventTypeLabel = Gtk2::Label->new($i18n->get('Event type:'));
	$SelectorHBox->pack_start($EventTypeLabel,0,0,0);
	$EventTypeLabel->show();
	# Combo selector
	my $EventType_Combo = Gtk2::ComboBox->new_text;
	# TRANSLATORS: Context: Event type: THIS STRING
	$EventType_Combo->insert_text(0, $i18n->get('Normal'));
	# TRANSLATORS: Context: Event type: THIS STRING
	$EventType_Combo->insert_text(1, $i18n->get('All day'));
	# TRANSLATORS: Context: Event type: THIS STRING
	$EventType_Combo->insert_text(2, $i18n->get('Birthday'));
	$SelectorHBox->pack_start($EventType_Combo,0,0,0);
	$EventType_Combo->show();

	# Create the tooltips widget
	my $Tooltips = Gtk2::Tooltips->new();
	$Tooltips->enable();
	# Set the tooltips
	$Tooltips->set_tip($OKButton, $i18n->get('Add this event'));
	$Tooltips->set_tip($CancelButton, $i18n->get('Discard this event'));
	
	# Create the widgets for the normal event selection
	my ($NormalEvent) = NormalEventWindow('NULL', $OKButton, $VBox_HBoxContainer, $Window);
	# Create the widgets for the all day event selection
	my ($AllDayEvent) = AllDayEventWindow('NULL', $VBox_HBoxContainer, $Window);
	# Create the widgets for the birthday event selection
	my ($BirthdayEvent) = BirthdayEventWindow(undef, $VBox_HBoxContainer, $Window);

	# Handle changed values in the combo box
	$EventType_Combo->signal_connect('changed' => sub {
			my $ActiveIndex = $EventType_Combo->get_active;
			if ($ActiveIndex == 0) {
				$BirthdayEvent->{MainVBox}->hide();
				$AllDayEvent->{MainVBox}->hide();
				$NormalEvent->{MainVBox}->show();
				$OKButton->signal_handler_disconnect($OKSignal) if defined($OKSignal);
				$OKSignal = $OKButton->signal_connect('clicked' => sub {
					NormalEvent_OK($Window, $NormalEvent, \%TimeHash, 0);
				});
			} elsif ($ActiveIndex == 1) {
				$NormalEvent->{MainVBox}->hide();
				$BirthdayEvent->{MainVBox}->hide();
				$AllDayEvent->{MainVBox}->show();
				$OKButton->signal_handler_disconnect($OKSignal) if defined($OKSignal);
				$OKSignal = $OKButton->signal_connect('clicked' => sub {
					AllDayEvent_OK($Window, $AllDayEvent, \%TimeHash, 0)});
			} elsif ($ActiveIndex == 2) {
				$NormalEvent->{MainVBox}->hide();
				$AllDayEvent->{MainVBox}->hide();
				$BirthdayEvent->{MainVBox}->show();
				$OKButton->signal_handler_disconnect($OKSignal) if defined($OKSignal);
				$OKSignal = $OKButton->signal_connect('clicked' => sub {
					AllDayEvent_OK($Window, $BirthdayEvent, \%TimeHash, 0,1)});
			}
		});

	$EventType_Combo->set_active(0);
	
	# Show the window
	$Window->show();
}

# Purpose: Edit the event currently selected in the eventlist
# Usage: EditEvent ();
sub EditEvent {
	my $Selected = [$EventlistWidget->get_selected_indices]->[0];
	# If $Selected isn't defined then nothing *is* selected and thus we just return (false)
	unless(defined($Selected)) {
		DPIntWarn('EditEvent() called with no event selected');
		return(0);
	}
	
	$MainWindow->set_sensitive(0);

	# Initialize
	my $EventUID = $EventlistWidget->{data}[$Selected][0];
	my $Time = $i18n->AMPM_To24($EventlistWidget->{data}[$Selected][1]);
	my $Tooltips = Gtk2::Tooltips->new();
	$Tooltips->enable();
	my $UIDObj = $iCalendar->get_info($EventUID);
	# Get date information from the UID
        my ($EventYear, $EventMonth, $EventDay) = iCal_ParseDateTime($UIDObj->{DTSTART});
	# Get date information from the calendar, we check if it differs with the one from
	# the UID
	my $Differs = 0;
	if($UIDObj->{RRULE}) {
		$Differs = 1;
	}
	my %TimeHash = (
		EventYear => $EventYear,
		EventMonth => $EventMonth,
		EventDay => $EventDay,
		OldEventYear => $EventYear,
		OldEventMonth => $EventMonth,
		OldEventDay => $EventDay,
		OldEventTime => $Time,
		Differs => $Differs,
	);
		
	# Create the main window with the appropriate OK/Cancel buttons and the event frame
	my ($Window, $VBox_HBoxContainer, $OKButton, $CancelButton);

	my $Type = GetEventListType();

	if ($Type eq 'normal') {
		# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
		# NORMAL EVENT
		# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

		($Window, $VBox_HBoxContainer, $OKButton, $CancelButton) = CreateEventContainerWin($i18n->get('Editing an event'), 'gtk-ok',$EventUID);
		# Create the widgets for the normal event selection
		my ($NormalEvent) = NormalEventWindow($EventUID, $OKButton, $VBox_HBoxContainer, $Window);
		$OKButton->signal_connect('clicked' => sub {
				NormalEvent_OK($Window, $NormalEvent, \%TimeHash, $EventUID);
		});
		
		# Show the default widget (Normal event)
		$NormalEvent->{MainVBox}->show();
	} elsif ($Type eq 'bday') {
		# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
		# BIRTHDAY EVENT
		# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
		($Window, $VBox_HBoxContainer, $OKButton, $CancelButton) = CreateEventContainerWin($i18n->get('Editing a birthday'), 'gtk-ok',$EventUID);
		
		my ($BirthdayEvent) = BirthdayEventWindow($EventUID, $VBox_HBoxContainer, $Window);
		$OKButton->signal_connect('clicked' => sub { AllDayEvent_OK($Window, $BirthdayEvent, \%TimeHash, $EventUID, 1)});
		# Show the main vbox
		$BirthdayEvent->{MainVBox}->show();
	} elsif ($Type eq 'allday') {
		# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
		# ALL DAY EVENT
		# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
		# Get the text string to use (it differs if we're editing on another date than
		# the selected

		($Window, $VBox_HBoxContainer, $OKButton, $CancelButton) = CreateEventContainerWin($i18n->get('Editing an all day event'), 'gtk-ok',$EventUID);
		my ($AllDayEvent) = AllDayEventWindow($EventUID, $VBox_HBoxContainer, $Window);
		$AllDayEvent->{MainVBox}->show();
		$OKButton->signal_connect('clicked' => sub { AllDayEvent_OK($Window, $AllDayEvent, \%TimeHash, $EventUID, 0)});
	} elsif ($Type eq 'holiday') {
		DPInfo(sprintf($i18n->get("This is a predefined \"holiday\" event. This event can not be edited directly from within Day Planner. In order to change these events use a text editor and edit the file %s."), $HolidayFile));
		$MainWindow->set_sensitive(1);
		return(1);
	} else {
		DPIntWarn("BUG!: EditEvent: \$Type contained invalid value: $Type");
		$MainWindow->set_sensitive(1);
		return(0);
	}
	
	$Tooltips->set_tip($CancelButton, $i18n->get('Discard changes'));
	$Tooltips->set_tip($OKButton, $i18n->get('Accept changes'));
			
	# Show the window
	$Window->show();
}

# NORMAL EVENT HANDLING FUNCTIONS

# Purpose: Create the widgets for editing a normal event
# Usage: my $NormalEvent = NormalEventWindow(UID, OK_BUTTON, VBOX_WIDGET, MAIN_WINDOW_WIDGET);
#
# UID is the event UID of the event being edited or undef
# OK_BUTTON is the OK button widget to attach to
# VBOX_WIDGET is the widget you want to pack our table into
# MAIN_WINDOW_WIDGET is the main window it will live inside
sub NormalEventWindow {
	# ==================================================================
	# INITIALIZE
	# ==================================================================
	my ($EventUID, $OKButton, $ParentVBox, $MyWindow) = @_;

	my $IsEditing = 0;
	
	my ($EventYear, $EventMonth, $EventDay) = $CalendarWidget->get_date();$EventMonth++;
	my ($EventSummary, $EventFulltext,$EventTime,$EventRRULE,$EventEXDATE);
	my ($OldEventYear, $OldEventMonth,$OldEventDay,$OldEventTime);
	
	# Check if the event is already defined, if it is then we assume that we're editing
	if($EventUID eq 'NULL') {
		$EventTime = '09:00';
	} else {
		my $EventHash = $iCalendar->get_info($EventUID);
		# We assume these two aren't empty. This will need to change if another part becomes obligatory
		($EventYear, $EventMonth, $EventDay, $EventTime) = iCal_ParseDateTime($EventHash->{DTSTART});
		$EventSummary = GetSummaryString($EventUID);
		$EventFulltext = $EventHash->{DESCRIPTION};
		$EventRRULE = $iCalendar->get_RRULE($EventUID);
		$EventEXDATE = $iCalendar->get_exceptions($EventUID);
		($OldEventYear, $OldEventMonth,$OldEventDay,$OldEventTime) = ($EventYear,$EventMonth,$EventDay,$EventTime);
		$IsEditing = 1;
	}

	# Create the tooltips widget
	my $Tooltips = Gtk2::Tooltips->new();
	$Tooltips->enable();
	
	# Create our main vbox
	my $MainVBox = Gtk2::VBox->new();
	$ParentVBox->pack_start($MainVBox,0,0,0);
	
	# Create the table inside the frame
	my $ContentTable = Gtk2::Table->new(2,3);
	$ContentTable->show();
	$MainVBox->pack_start($ContentTable,1,1,0);
	
	# ==================================================================
	# ADD THE DATE/TIME/SUMMARY BOXES
	# ==================================================================
	
	# Create the time label
	my $TimeLabel = Gtk2::Label->new($i18n->get('Time:'));
	$TimeLabel->show();
	$ContentTable->attach_defaults($TimeLabel, 0,1,0,1);
	
	# Time entry box
	my ($HourSpinner, $MinuteSpinner, $TimeHBox, $AMPM) = TimeSelection($EventTime);
	# Attach them to our main widget
	$ContentTable->attach_defaults($TimeHBox, 1,2,0,1);
	
	# Create the date label
	my $DateLabel = Gtk2::Label->new($i18n->get('Date:'));
	$DateLabel->show();
	$ContentTable->attach_defaults($DateLabel, 0,1,1,2);
	# Date  entry box
	my ($DateTable, $DateEntry) = DateSelection($EventUID);
	# Attach them to our main widget
	$ContentTable->attach_defaults($DateTable, 1,2,1,2);
	
	# Create the summary label
	my $SummaryLabel = Gtk2::Label->new($i18n->get('Description:'));
	$SummaryLabel->show();
	$ContentTable->attach_defaults($SummaryLabel, 0,1,2,3);

	# Create the summary entry box
	my $SummaryEntry = Gtk2::Entry->new();
	if (defined($EventSummary) and length($EventSummary)) {
		$SummaryEntry->set_text($EventSummary);
	}
	$SummaryEntry->set_activates_default(1);
	$SummaryEntry->show();
	$ContentTable->attach_defaults($SummaryEntry, 1,2,2,3);
	$Tooltips->set_tip($SummaryEntry, $i18n->get('Enter a description of the event here'));
	
	# ==================================================================
	# ADD THE EXTENDED ENTRY
	# ==================================================================
	my $FulltextWindow;
	
	my ($FT_Expander, $FulltextEntry,$RecurCombo,$ExceptEntry,$StopAt_Entry) = CreateAdvancedWidget($EventRRULE,$EventEXDATE);
	$MainVBox->pack_start($FT_Expander,0,0,0);
	# Add text to it if needed
	if (defined($EventFulltext) and $EventFulltext =~ /\S/) {
		my $AddEventBuffer = Gtk2::TextBuffer->new();
		$AddEventBuffer->set_text($EventFulltext);
		$FulltextEntry->set_buffer($AddEventBuffer);
		if(not $FT_Expander->get_expanded) {
			$FT_Expander->signal_emit('activate');
		}
	}

	# Create a hash containing the widgets that the caller might need
	# and return it.
	my %ReturnHash;
	$ReturnHash{MainVBox} = $MainVBox;
	$ReturnHash{HourSpinner} = $HourSpinner;
	$ReturnHash{MinuteSpinner} = $MinuteSpinner;
	$ReturnHash{AMPM} = $AMPM;
	$ReturnHash{SummaryEntry} = $SummaryEntry;
	$ReturnHash{FulltextEntry} = $FulltextEntry;
	$ReturnHash{RecurCombo} = $RecurCombo;
	$ReturnHash{ExceptEntry} = $ExceptEntry;
	$ReturnHash{StopAt_Entry} = $StopAt_Entry;
	$ReturnHash{DateEntry} = $DateEntry;
	return(\%ReturnHash);
}

# Purpose: Handle the OK button for the Normal event widget
# Usage: $OKButton->signal_connect('clicked' => sub { NormalEvent_OK($MainWindow, $WidgetHash, \%TimeHash, EventUID)});
#
# EventUID can be undef.
sub NormalEvent_OK {
	my ($MyWindow, $WidgetHash, $TimeHash, $EventUID) = @_;
	my $Error = 0;
	
	# Get the contents
	my $Time = GetTimeFromWidgets($WidgetHash->{HourSpinner},$WidgetHash->{MinuteSpinner}, $WidgetHash->{AMPM});
	my $Summary = $WidgetHash->{SummaryEntry}->get_text;
	my $FulltextBuff = $WidgetHash->{FulltextEntry}->get_buffer;
	my $Fulltext = $FulltextBuff->get_text($FulltextBuff->get_bounds,1);

	# Get variables from the TimeHash
	my $OldEventYear = ${$TimeHash}{OldEventYear};
	my $OldEventMonth = ${$TimeHash}{OldEventMonth};
	my $OldEventDay = ${$TimeHash}{OldEventDay};
	my $OldEventTime = ${$TimeHash}{OldEventTime};
	# Get time
	my($EventYear,$EventMonth,$EventDay) = GetDateFromEntry($WidgetHash->{DateEntry});
	
	# TODO: Add tests for an event that is *identical*
	unless ($Summary) {
		DPError($i18n->get('There is no description of this event. Please enter a description.'));
		$Error = 1;
	}
	if(not $EventYear and not $Error) {
		DPError($i18n->get('The date you have entered is invalid.'));
		$Error = 1;
	}
	# We don't do this if an error occurred
	unless ($Error) {
		my %EntryHash;
		# Add them to the hash
		$EntryHash{SUMMARY} = $Summary;
		$EntryHash{DESCRIPTION} = $Fulltext;
		$EntryHash{DTSTART} = iCal_GenDateTime($EventYear, $EventMonth, $EventDay, $Time);
		$EntryHash{DTEND} = iCal_GenDateTime($EventYear, $EventMonth, $EventDay, $Time);
		$EntryHash{RRULE} = GetRRULEFromCombo($WidgetHash);
		foreach my $EXDATE (@{GetExceptionsFromEntry($WidgetHash->{ExceptEntry})}) {
			if(not $EntryHash{EXDATE}) {
				$EntryHash{EXDATE} = [];
			}
			push(@{$EntryHash{EXDATE}}, $EXDATE);
		}

		if($EventUID) {
			$iCalendar->change($EventUID,%EntryHash);
		} else {
			$iCalendar->add(%EntryHash);
		}
		
		# Call UpdatedData to tell Day Planner to save the data and redraw widgets
		UpdatedData();
		# Destroy our window, it will get the signal and do anything it needs to
		$MyWindow->destroy;
		$MainWindow->set_sensitive(1);
	}
}

# ALL DAY AND BIRTHDAY EVENT HANDLING FUNCTIONS

# Purpose: Create the widgets for editing an all-day event
# Usage: my ($AllDayEventWidget, $SummaryWidget, $DetailsWidget) = AllDayEventWindow(UID, VBOX_WIDGET, MAIN_WINDOW_WIDGET, WIDGET HASHREF);
#
# UID is the UID of the event you're editing or undef
# VBOX_WIDGET is the widget you want to pack our table into
# MAIN_WINDOW_WIDGET is the main window it will live inside
#
# TODO: Make AllDayEventWindow and NormalEventWindow share the same
# summary and fulltext entries!
sub AllDayEventWindow {
	# ==================================================================
	# INITIALIZE
	# ==================================================================
	my ($EventUID, $ParentVBox, $MyWindow) = @_;

	my ($EventYear, $EventMonth, $EventDay) = $CalendarWidget->get_date();$EventMonth++;
	my ($EventSummary, $EventFulltext,$EventTime,$EventRRULE,$EventEXDATE);
	my ($OldEventYear, $OldEventMonth,$OldEventDay,$OldEventTime);
	
	# Check if the event is already defined, if it is then we assume that we're editing
	if(not $EventUID eq 'NULL') {
		my $EventHash = $iCalendar->get_info($EventUID);
		# We assume these two aren't empty. This will need to change if another part becomes obligatory
		my ($Year, $Month, $Day) = iCal_ParseDateTime($EventHash->{DTSTART});
		$EventSummary = GetSummaryString($EventUID);
		$EventFulltext = $EventHash->{DESCRIPTION};
		$EventRRULE = $iCalendar->get_RRULE($EventUID);
		$EventEXDATE = $iCalendar->get_exceptions($EventUID);
		($OldEventYear, $OldEventMonth,$OldEventDay) = ($EventYear,$EventMonth,$EventDay);
	}

	# Create the tooltips widget
	my $Tooltips = Gtk2::Tooltips->new();
	$Tooltips->enable();
	
	# Create our main vbox
	my $MainVBox = Gtk2::VBox->new();
	$ParentVBox->pack_start($MainVBox,0,0,0);
	
	# Create the table inside the frame
	my $ContentTable = Gtk2::Table->new(2,2);
	$ContentTable->show();
	$MainVBox->pack_start($ContentTable,1,1,0);
	
	# ==================================================================
	# ADD THE DATE/SUMMARY BOXES
	# ==================================================================
	# Create the date label
	my $DateLabel = Gtk2::Label->new($i18n->get('Date:'));
	$DateLabel->show();
	$ContentTable->attach_defaults($DateLabel, 0,1,0,1);
	# Date  entry box
	my ($DateTable, $DateEntry) = DateSelection($EventUID);
	# Attach them to our main widget
	$ContentTable->attach_defaults($DateTable, 1,2,0,1);
	
	# Create the summary label
	my $SummaryLabel = Gtk2::Label->new($i18n->get('Description:'));
	$SummaryLabel->show();
	$ContentTable->attach_defaults($SummaryLabel, 0,1,1,2);

	# Create the summary entry box
	my $SummaryEntry = Gtk2::Entry->new();
	if (defined($EventSummary) and length($EventSummary)) {
		$SummaryEntry->set_text($EventSummary);
	}
	$SummaryEntry->set_activates_default(1);
	$SummaryEntry->show();
	$ContentTable->attach_defaults($SummaryEntry, 1,2,1,2);
	$Tooltips->set_tip($SummaryEntry, $i18n->get('Enter a description of the event here'));
	
	# ==================================================================
	# ADD THE EXTENDED ENTRY
	# ==================================================================
	my $FulltextWindow;
	
	my ($FT_Expander, $FulltextEntry,$RecurCombo,$ExceptEntry,$StopAt_Entry) = CreateAdvancedWidget($EventRRULE,$EventEXDATE);
	$Tooltips->set_tip($FT_Expander, $i18n->get('Additional information'));
	$MainVBox->pack_start($FT_Expander,0,0,0);
	
	# Add text to it if needed
	if (defined($EventFulltext) and $EventFulltext =~ /\S/) {
		my $AddEventBuffer = Gtk2::TextBuffer->new();
		$AddEventBuffer->set_text($EventFulltext);
		$FulltextEntry->set_buffer($AddEventBuffer);
		# Expand it if it isn't already
		if(not $FT_Expander->get_expanded) {
			$FT_Expander->signal_emit('activate');
		}
	}
	
	my %ReturnHash;
	$ReturnHash{MainVBox} = $MainVBox;
	$ReturnHash{SummaryEntry} = $SummaryEntry;
	$ReturnHash{FulltextEntry} = $FulltextEntry;
	$ReturnHash{RecurCombo} = $RecurCombo;
	$ReturnHash{ExceptEntry} = $ExceptEntry;
	$ReturnHash{StopAt_Entry} = $StopAt_Entry;
	$ReturnHash{DateEntry} = $DateEntry;
	
	return(\%ReturnHash);
}

# Purpose: Handle the OK button for the all day event widget and birthday event widget
# Usage: $OKButton->signal_connect('clicked' => sub { AllDayEvent_OK($MainWindow, $SummaryEntry, $FulltextEntry, \%TimeHash, EventUID, IS_BIRTHDAY)});
#
# EventUID can be empty. If empty then add-mode is used.
#
# IS_BIRTHDAY can be either true or false. If the entry being changed/added is a birthday
# then it should be true.
sub AllDayEvent_OK {
	my ($MyWindow, $WidgetHash,$TimeHash, $EventUID,$IsBirthday) = @_;
	my $Error = 0;
	
	# Get the contents of the summary
	my $Summary = $WidgetHash->{SummaryEntry}->get_text;

	# Get variables from the TimeHash
	my $OldEventYear = ${$TimeHash}{OldEventYear};
	my $OldEventMonth = ${$TimeHash}{OldEventMonth};
	my $OldEventDay = ${$TimeHash}{OldEventDay};
	# From the widget hash
	my($EventYear,$EventMonth,$EventDay) = GetDateFromEntry($WidgetHash->{DateEntry});
	
	# TODO: Add tests for an event that is *identical* (wouldn't this be a job for DP::iCalendar ?)
	if (not $Summary) {
		DPError($i18n->get('There is no description of this event. Please enter a description.'));
		$Error = 1;
	}
	if(not $EventYear and not $Error) {
		DPError($i18n->get('The date you have entered is invalid.'));
		$Error = 1;
	}
	# We don't do this if an error occurred
	unless ($Error) {
		my %EntryHash;
		# Add them to the hash
		$EntryHash{DTSTART} = iCal_GenDateTime($EventYear, $EventMonth, $EventDay);
		$EntryHash{DTEND} = iCal_GenDateTime($EventYear, $EventMonth, $EventDay);
		if($IsBirthday) {
			$EntryHash{'X-DP-BIRTHDAY'} = 'TRUE';
			$EntryHash{'X-DP-BIRTHDAYNAME'} = $Summary;
			$EntryHash{'X-DP-BORNATDTSTART'} = 'TRUE';
			$EntryHash{RRULE} = 'FREQ=YEARLY';
			$EntryHash{SUMMARY} = $i18n->get_advanced("%(name)'s birthday", { name => $Summary });
		} else {
			$EntryHash{SUMMARY} = $Summary;
			$EntryHash{RRULE} = GetRRULEFromCombo($WidgetHash);
			# Get and add content to the fulltext
			my $FulltextBuff = $WidgetHash->{FulltextEntry}->get_buffer;
			my $Fulltext = $FulltextBuff->get_text($FulltextBuff->get_bounds,1);
			$EntryHash{DESCRIPTION} = $Fulltext;
			# Get exceptions
			foreach my $EXDATE (@{GetExceptionsFromEntry($WidgetHash->{ExceptEntry})}) {
				if(not $EntryHash{EXDATE}) {
					$EntryHash{EXDATE} = [];
				}
				push(@{$EntryHash{EXDATE}}, $EXDATE);
			}
		}
		if($EventUID) {
			$iCalendar->change($EventUID,%EntryHash);
		} else {
			$iCalendar->add(%EntryHash);
		}
		
		# Call UpdatedData to tell Day Planner to save the data and redraw widgets
		UpdatedData();
		# Destroy our window, it will get the signal and do anything it needs to
		$MyWindow->destroy;
		$MainWindow->set_sensitive(1);
	}
}

# Purpose: Create the widgets for editing a birthday event
# Usage: my ($BirthdayEventWidget, $SummaryWidget) = BirthdayEventWindow(UID, VBOX_WIDGET, MAIN_WINDOW_WIDGET);
#
# UID is the event UID of the event being edited or undef
# VBOX_WIDGET is the widget you want to pack our widgets into
# MAIN_WINDOW_WIDGET is the main window it will live inside
sub BirthdayEventWindow {
	# ==================================================================
	# INITIALIZE
	# ==================================================================
	my ($UID, $ParentVBox, $MyWindow) = @_;
	my ($EventYear, $EventMonth, $EventDay) = $CalendarWidget->get_date();$EventMonth++;
	
	# Create our main vbox
	my $MainVBox = Gtk2::VBox->new();
	$ParentVBox->pack_start($MainVBox,0,0,0);
	
	# Create the tooltips widget
	my $Tooltips = Gtk2::Tooltips->new();
	$Tooltips->enable();
	
	# Create the table inside the frame
	my $ContentTable = Gtk2::Table->new(2,2);
	$ContentTable->show();
	$MainVBox->pack_start($ContentTable,1,1,0);
	
	# ==================================================================
	# ADD THE DATE/SUMMARY BOXES
	# ==================================================================
	# Create the date label
	my $DateLabel = Gtk2::Label->new($i18n->get('Born:'));
	$DateLabel->show();
	$ContentTable->attach_defaults($DateLabel, 0,1,0,1);
	# Date  entry box
	my ($DateTable, $DateEntry) = DateSelection($UID);
	# Attach them to our main widget
	$ContentTable->attach_defaults($DateTable, 1,2,0,1);
	
	# Create the summary label
	# TRANSLATORS: Followed by a field where you enter the name of the person whose birthday it is
	my $SummaryLabel = Gtk2::Label->new($i18n->get('Name:'));
	$SummaryLabel->show();
	$ContentTable->attach_defaults($SummaryLabel, 0,1,1,2);

	# Create the summary entry box
	my $SummaryEntry = Gtk2::Entry->new();
	if (defined($UID) and length($UID)) {
		my $Info = $iCalendar->get_info($UID);
		$SummaryEntry->set_text($Info->{'X-DP-BIRTHDAYNAME'});
	}
	$SummaryEntry->set_activates_default(1);
	$SummaryEntry->show();
	$ContentTable->attach_defaults($SummaryEntry, 1,2,1,2);
	$Tooltips->set_tip($SummaryEntry, $i18n->get('Enter the name of the person whose birthday it is here'));
	
	# Create a hash containing the widgets that the caller might need
	# and return it.
	my %ReturnHash;
	$ReturnHash{MainVBox} = $MainVBox;
	$ReturnHash{SummaryEntry} = $SummaryEntry;
	$ReturnHash{DateEntry} = $DateEntry;
	return(\%ReturnHash);
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Main GUI functions
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# USER DIALOGS

# Purpose: Display the about dialog
# Usage: AboutBox();
sub AboutBox {
	$MainWindow->set_sensitive(0);
	my $AboutDialog = Gtk2::AboutDialog->new;
	$AboutDialog->set_transient_for($MainWindow);
	$AboutDialog->set_position('center-on-parent');
	$AboutDialog->set_authors('Eskild Hustvedt <eskild at zerodogg dot org>');
	$AboutDialog->set_artists('Jason Holland (original icon and logo)' . "\n" .
					'Andreas Nilsson (small calendar icon)' . "\n" .
					'The Tango Project - http://tango.freedesktop.org/'
				);
	$AboutDialog->set_copyright('Copyright (C) Eskild Hustvedt 2006, 2007, 2008');
	$AboutDialog->set_url_hook(\&LaunchWebBrowser);
	$AboutDialog->set_website('http://www.day-planner.org/');
	# TRANSLATORS: Feel free to localize the application name "Day Planner" to your language.
	#		For instance "Dagsplanleggar" for Norwegian.
	my $AppName = $i18n->get('Day Planner');
	# Use set_program_name instead of set_name in Gtk2 2.12+
	if(Gtk2->CHECK_VERSION(2,12,0)) {
		$AboutDialog->set_program_name($AppName);
	}
	else
	{
		$AboutDialog->set_name($AppName);
	}
	$AboutDialog->set_version($Version);
	if ($VersionName eq 'SVN')
	{
		if (not $Version =~ /svn/i)
		{
			$AboutDialog->set_version($Version.' SVN');
		}
	}
	# GPL summary, should never be marked as translateable
	$AboutDialog->set_license("Day Planner is free software: you can redistribute it and/or\nmodify it under the terms of the GNU General Public License\nas published by the Free Software Foundation, either version 3\nof the License, or (at your option) any later version.\n\nDay Planner is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty\nof MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nYou should have recieved a copy of the GNU General Public License\nalong with this program. If not, see <http://www.gnu.org/licenses/>.");
	# Logo
	my $LogoImage = DetectImage('dayplanner-about.png','dayplanner-48x48.png','dayplanner-32x32.png','dayplanner-24x24.png', 'dayplanner-16x16.png', 'dayplanner.png','dayplanner_HC48.png','dayplanner_HC24.png', 'dayplanner_HC16.png', );
	if ($LogoImage) {
		my $PixBuf = Gtk2::Gdk::Pixbuf->new_from_file($LogoImage);
		$AboutDialog->set_logo($PixBuf);
	}
	# Translator credits
	unless ($i18n->get('THE NAMES OF THE TRANSLATORS') eq 'THE NAMES OF THE TRANSLATORS') {
		$AboutDialog->set_translator_credits($i18n->get('THE NAMES OF THE TRANSLATORS'));
	}
	$AboutDialog->run;
	$AboutDialog->destroy();
	$MainWindow->set_sensitive(1);
}

# Purpose: Draw the preferences window and allow the user to set different
#  configuration options
# Usage: PreferencesWindow(NAME);
sub PreferencesWindow {
	$_[0] =~ s/_//;
	my $Name = $_[0];
	$MainWindow->set_sensitive(0);
	my $PreferencesWindow = Gtk2::Window->new();
	$PreferencesWindow->set_modal(1);
	$PreferencesWindow->set_transient_for($MainWindow);
	$PreferencesWindow->set_position('center-on-parent');
	$PreferencesWindow->set_title($Name);
	$PreferencesWindow->set_resizable(0);
	$PreferencesWindow->set_border_width(12);
	$PreferencesWindow->set_skip_taskbar_hint(1);
	$PreferencesWindow->set_skip_pager_hint(1);
	my $Tooltips = Gtk2::Tooltips->new();
	$Tooltips->enable();
	
	# Create the main VBox
	my $MainVBox = Gtk2::VBox->new();
	$MainVBox->show();
	$PreferencesWindow->add($MainVBox);
	
	# Create the notebook
	my $Notebook = Gtk2::Notebook->new();
	$Notebook->show();
	$MainVBox->pack_start($Notebook,0,0,0);
	
	# ==================================================================
	# ==================================================================
	# REMINDERS TAB
	# ==================================================================
	# ==================================================================
	
	# Create the vbox
	my $Config_VBox = Gtk2::VBox->new();
	$Config_VBox->show();
	$Notebook->append_page($Config_VBox,$i18n->get('Reminders'));
	$Config_VBox->set_border_width(12);
	
	# A simple hash we use to parse back and forth between the PreNotification
	# combo box and the config option itself
	my %PreNotificationParser = (
		0 => '10min',
		1 => '20min',
		2 => '30min',
		3 => '45min',
		4 => '1hr',
		5 => '2hrs',
		6 => '4hrs',
		7 => '6hrs',
		
		'10min' => 0,
		'20min' => 1,
		'30min' => 2,
		'45min' => 3,
		'1hr' => 4,
		'1hrs' => 4,
		'2hr' => 5,
		'2hrs' => 5,
		'4hr' => 6,
		'4hrs' => 6,
		'6hr' => 7,
		'6hrs' => 7,

		'default' => 3,
		'readable_default' => '45mins',
	);
	
	# If the reminder (daemon) should autostart on login or not
	my $AutostartReminder_Checkbox = Gtk2::CheckButton->new_with_label($i18n->get('Start the Day Planner reminder automatically when logging in'));
	$AutostartReminder_Checkbox->show();
	$Config_VBox->pack_start($AutostartReminder_Checkbox,0,0,0);
	$AutostartReminder_Checkbox->set_active(1) if $InternalConfig{AddedAutostart};
	$AutostartReminder_Checkbox->signal_connect('toggled' => sub {
			if($AutostartReminder_Checkbox->get_active) {
				DP_AddAutostart();
			} else {
				unless(DP_RemoveAutostart()) {
					$AutostartReminder_Checkbox->set_active(1);
				}
			}
		});
	

	# The label that explains about notifications
	my $NotificationLabel = Gtk2::Label->new($i18n->get('When an event is coming up, remind me:'));
	$NotificationLabel->show();
	$NotificationLabel->set_justify('left');
	$NotificationLabel->set_alignment(0,0);
	$Config_VBox->pack_start($NotificationLabel,0,0,0);
	
	# If the user wants to be notified in advance
	my $NotifyAdvance_CheckBox = Gtk2::CheckButton->new_with_label($i18n->get('One day in advance'));
	$NotifyAdvance_CheckBox->show();
	$Config_VBox->pack_start($NotifyAdvance_CheckBox,0,0,0);
	$NotifyAdvance_CheckBox->signal_connect('toggled' => sub {
			$UserConfig{Events_DayNotify} = $NotifyAdvance_CheckBox->get_active();
		});
	if($UserConfig{Events_DayNotify}) {
		$NotifyAdvance_CheckBox->set_active(1);
	}
	
	# The widgets that selects when the user wants to be notified
	# The HBox
	my $TimeSel_HBox = Gtk2::HBox->new();
	$Config_VBox->pack_start($TimeSel_HBox,0,0,0);
	$TimeSel_HBox->show();
	# The checkbutton
	my $EnableDisablePreNotCB = Gtk2::CheckButton->new();
	$EnableDisablePreNotCB->show();
	$TimeSel_HBox->pack_start($EnableDisablePreNotCB,0,0,0);

	# The combo box
	my $TimeSel_Combo = Gtk2::ComboBox->new_text();
	$TimeSel_Combo->insert_text(0, sprintf($i18n->get('%s minutes'),'10'));
	$TimeSel_Combo->insert_text(1, sprintf($i18n->get('%s minutes'),'20'));
	$TimeSel_Combo->insert_text(2, sprintf($i18n->get('%s minutes'),'30'));
	$TimeSel_Combo->insert_text(3, sprintf($i18n->get('%s minutes'),'45'));
	$TimeSel_Combo->insert_text(4, sprintf($i18n->get('%s hour'),'1'));
	$TimeSel_Combo->insert_text(5, sprintf($i18n->get('%s hours'),'2'));
	$TimeSel_Combo->insert_text(6, sprintf($i18n->get('%s hours'),'4'));
	$TimeSel_Combo->insert_text(7, sprintf($i18n->get('%s hours'),'6'));
	# Set the value
	if($UserConfig{Events_NotifyPre} eq '0') {
		$TimeSel_Combo->set_active(3);
	} elsif (defined($PreNotificationParser{$UserConfig{Events_NotifyPre}})) {
		$TimeSel_Combo->set_active($PreNotificationParser{$UserConfig{Events_NotifyPre}});
	} else {
		$TimeSel_Combo->set_active($PreNotificationParser{'default'});
	}
	$TimeSel_Combo->show();
	$TimeSel_HBox->pack_start($TimeSel_Combo,0,0,0);
	# Register the changed signal
	$TimeSel_Combo->signal_connect('changed' => sub {
				$UserConfig{Events_NotifyPre} = $PreNotificationParser{$TimeSel_Combo->get_active};
			});
	# TRANSLATORS: See the preferences window, this is preceeded by X minutes or X hour(s).
	my $TimeSel_Label = Gtk2::Label->new(' ' . $i18n->get('before the event time'));
	$TimeSel_Label->show();
	$TimeSel_HBox->pack_start($TimeSel_Label,0,0,0);
	
	# Set up the checkbutton signals and defaults
	$EnableDisablePreNotCB->signal_connect('toggled' => sub {
			if($EnableDisablePreNotCB->get_active()) {
				$TimeSel_Combo->set_sensitive(1);
				$TimeSel_Combo->signal_emit('changed');
			} else {
				$UserConfig{Events_NotifyPre} = 0;
				$TimeSel_Combo->set_sensitive(0);
			}});

	if($UserConfig{Events_NotifyPre}) {
		$EnableDisablePreNotCB->set_active(1);
		$EnableDisablePreNotCB->signal_emit('toggled');
	} else {
		$EnableDisablePreNotCB->set_active(0);
		$EnableDisablePreNotCB->signal_emit('toggled');
	}
	
	
	# ==================================================================
	# ==================================================================
	# SYNCHRONIZATION TAB
	# ==================================================================
	# ==================================================================
	
	# Get and set the values we're going to use
	my $Host = $UserConfig{DPS_host} ? $UserConfig{DPS_host} : '';
	my $Port = $UserConfig{DPS_port} ? $UserConfig{DPS_port} : 4435;
	my $Username = $UserConfig{DPS_user} ? $UserConfig{DPS_user} : '';
	my $Password = $UserConfig{DPS_pass} ? $UserConfig{DPS_pass} : '';
	my $DPSWasStatus = $UserConfig{DPS_enable};

	# Create the vbox
	my $Sync_VBox = Gtk2::VBox->new();
	$Sync_VBox->show();
	$Sync_VBox->set_border_width(12);
	$Notebook->append_page($Sync_VBox,$i18n->get('Synchronization'));

	# Create the table
	my $Config_Table = Gtk2::Table->new(3,4);
	$Config_Table->show();

	# Enable/disable checkbox
	my $EnableDisableCButton = Gtk2::CheckButton->new($i18n->get('Automatically synchronize an online copy of this calendar'));
	$Sync_VBox->pack_start($EnableDisableCButton,0,0,0);
	$EnableDisableCButton->signal_connect('toggled' => sub {
			if($EnableDisableCButton->get_active) {
				if(DPS_SSLSocketTest()) {
					$UserConfig{DPS_enable} = 1;
					$Config_Table->set_sensitive(1);
				}
			} else {
				$UserConfig{DPS_enable} = 0;
				$Config_Table->set_sensitive(0);
			}});
	if($UserConfig{DPS_enable}) {
		$EnableDisableCButton->set_active(1);
		$EnableDisableCButton->signal_emit('toggled');
	} else {
		$EnableDisableCButton->set_active(0);
		$EnableDisableCButton->signal_emit('toggled');
	}
	$EnableDisableCButton->show();

	# Add the table to the UI
	$Sync_VBox->pack_start($Config_Table,0,0,0);
	
	# ==================================================================
	# HOST/PORT
	# ==================================================================
	
	# Host

	#  Label
	my $UseHost = Gtk2::Label->new($i18n->get('Server:'));
	$UseHost->show();
	$Config_Table->attach_defaults($UseHost, 0,1,0,1);
	
	#  Entry
	my $HostEntry = Gtk2::Entry->new();
	$HostEntry->set_text($Host);
	$HostEntry->show();
	$Config_Table->attach_defaults($HostEntry, 1,2,0,1);
	
	# Port
	
	#  Label
	my $UsePort = Gtk2::Label->new(' ' . $i18n->get('Port:'));
	$UsePort->show();
	$Config_Table->attach_defaults($UsePort, 2,3,0,1);
	
	#  Spinner
	my $PortAdjustment = Gtk2::Adjustment->new(0.0, 0.0, 65000.0, 1.0, 5.0, 0.0);
	my $PortSpinner = Gtk2::SpinButton->new($PortAdjustment,0,0);
	$PortSpinner->set_value($Port);
	$PortSpinner->show();
	$Config_Table->attach_defaults($PortSpinner, 3,4,0,1);
	
	# ==================================================================
	# USERNAME/PASSWORD
	# ==================================================================
	
	# Username
	#  Label
	my $UsernameLabel = Gtk2::Label->new($i18n->get('Username:'));
	$UsernameLabel->show();
	$Config_Table->attach_defaults($UsernameLabel, 0,1,1,2);
	
	#  Entry
	my $UserEntry = Gtk2::Entry->new();
	$UserEntry->set_text($Username);
	$UserEntry->show();
	$Config_Table->attach_defaults($UserEntry, 1,4,1,2);
	
	# Password
	#  Label
	my $PasswordLabel = Gtk2::Label->new($i18n->get('Password:'));
	$PasswordLabel->show();
	$Config_Table->attach_defaults($PasswordLabel, 0,1,2,3);
	
	#  Entry
	my $PasswordEntry = Gtk2::Entry->new();
	$PasswordEntry->set_text($Password);
	$PasswordEntry->show();
	$PasswordEntry->set_visibility(0);
	$Config_Table->attach_defaults($PasswordEntry, 1,4,2,3);
	
	# ==================================================================
	# FINALIZE WINDOW
	# ==================================================================
	my $ClosePerform = sub {
			if(not defined($UserConfig{DPS_user}) or (not($UserConfig{DPS_user} eq $UserEntry->get_text()))) {
				$UserConfig{DPS_user} = $UserEntry->get_text();
				$DPSWasStatus = 0;
			}
			if(not defined($UserConfig{DPS_pass}) or (not($UserConfig{DPS_pass} eq $PasswordEntry->get_text()))) {
				$UserConfig{DPS_pass} = $PasswordEntry->get_text();
				$DPSWasStatus = 0;
			}
			if(not defined($UserConfig{DPS_host}) or (not($UserConfig{DPS_host} eq $HostEntry->get_text()))) {
				$UserConfig{DPS_host} = $HostEntry->get_text();
				$DPSWasStatus = 0;
			}
			if(not defined($UserConfig{DPS_port}) or not($UserConfig{DPS_port} eq $PortSpinner->get_value())) {
				# Don't write the port if it is currently empty and DPS isn't enabled
				if (defined($UserConfig{DPS_port}) or ($UserConfig{DPS_enable})) {
					$UserConfig{DPS_port} = $PortSpinner->get_value();
					$DPSWasStatus = 0;
				}
			}
			# Make sure that the proper DPS settings are in place
			if($UserConfig{DPS_enable}) {
				if(not $UserConfig{DPS_user} or not $UserConfig{DPS_pass} or not $UserConfig{DPS_host} or not $UserConfig{DPS_port}) {
					DPError($i18n->get('You have not entered the information required for synchronization to be enabled, it has been disabled.'));
					$UserConfig{DPS_enable} = 0;
				}
			}
			WriteConfig();
			$PreferencesWindow->hide();
			# Synchronize if DPS settings have been changed and it is enabled
			if($UserConfig{DPS_enable} and not $DPSWasStatus) {
				DPS_Perform('SYNC');
			}
			$PreferencesWindow->destroy();
			$MainWindow->set_sensitive(1);
		};
	# Handle closing
	$PreferencesWindow->signal_connect('delete-event' => $ClosePerform);
	
	# Add the buttons
	my $ButtonHBox = Gtk2::HBox->new();
	$ButtonHBox->show();
	$MainVBox->pack_start($ButtonHBox,0,0,0);

	my $CloseButton = Gtk2::Button->new_from_stock('gtk-close');
	$CloseButton->signal_connect('clicked' => $ClosePerform);
	$CloseButton->show();
	$ButtonHBox->pack_end($CloseButton,0,0,0);

	# Show the config window
	$PreferencesWindow->show();
}

# MAIN WINDOW

# Purpose: Populate the event list for the currently selected date in the calendar
# Usage: PopulateEventList();
sub PopulateEventList {
	@{$EventlistWidget->{data}} = ();
	my $Year = $CalendarWidget->year;
	my $Month = $CalendarWidget->month;$Month++;
	my $Day = $CalendarWidget->selected_day;
	
	# New eventlist so make the toolbar edit button and edit/delete menu entries insensitive
	$ToolbarEditButton->set_sensitive(0);
	$ToolbarDeleteButton->set_sensitive(0);
	$MenuEditEntry->set_sensitive(0);
	$MenuDeleteEntry->set_sensitive(0);
	
	# Main calendar contents
	if (my $TimeArray = $iCalendar->get_dateinfo($Year,$Month,$Day)) {
		foreach my $Time (sort @{$TimeArray}) {
			foreach my $UID (@{$iCalendar->get_timeinfo($Year,$Month,$Day,$Time)}) {
				my $EventSummary = GetSummaryString($UID);
				# Don't add Time = DAY.
				if($Time eq 'DAY') {
					$Time = '';
				}
				push (@{$EventlistWidget->{data}}, [$UID,$i18n->AMPM_From24($Time), $EventSummary]);
			}
		}
	}
	# Fetch the Holidays if the year is above 1970 (UNIX epoch) and 2037 (UNIX epoch overflow)
	#
	# TODO: REMOVE
#	unless($Year > 2037) {
#		unless($Year < 1970) {
#			unless(defined($HolidayParser)) {
#				$HolidayParser = Date::HolidayParser->new($HolidayFile);
#			}
#			unless(defined($Holidays{$Year})) {
#					$Holidays{$Year} = $HolidayParser->get($Year);
#			}
#			if(defined($Holidays{$Year}) and defined($Holidays{$Year}->{$Month}) and defined($Holidays{$Year}->{$Month}{$Day})) {
#				foreach my $CurrHoliday (keys(%{$Holidays{$Year}->{$Month}{$Day}})) {
#					push(@{$EventlistWidget->{data}}, [0,'', $CurrHoliday]);
#				}
#			}
#		}
#
#	}
	$EventlistWidget->show();
}

# Purpose: Draw the eventlist on the currently selected date in the calendar
# Usage: DrawEventlist();
sub DrawEventlist {
	# Greate the pop-up on right-click
	my $PopupWidget = Gtk2::Menu->new();
	my $delete = Gtk2::ImageMenuItem->new_from_stock('gtk-delete');
	$delete->show();
	$delete->signal_connect('activate' => \&DeleteEvent);
	my $edit = Gtk2::ImageMenuItem->new_from_stock('gtk-edit');
	$edit->show();
	$edit->signal_connect('activate' => \&EditEvent);
	$PopupWidget->append($edit);
	$PopupWidget->append($delete);
	$PopupWidget->show();

	# Create the widget and set up signal handlers
	$EventlistWidget = Gtk2::SimpleList->new (
		'UID' => 'hidden',
		# TRANSLATORS: This is followed by the time of an event
		$i18n->get('Time') => 'text',
		$i18n->get('Event') => 'text',
	);
	$EventlistWidget->set_rules_hint(1);
	$EventlistWidget->signal_connect('row_activated' => \&EditEvent);
	$EventlistWin->add($EventlistWidget);
	# Handle making the toolbar edit button sensitive
	$EventlistWidget->signal_connect('focus-in-event' => sub {
		# If there is something in that data array then there is something selected on focus-in-event
		if(@{$EventlistWidget->{data}}) {
			$ToolbarEditButton->set_sensitive(1);
			$ToolbarDeleteButton->set_sensitive(1);
			$MenuEditEntry->set_sensitive(1);
			$MenuDeleteEntry->set_sensitive(1);
		}
	});
	$EventlistWidget->signal_connect('button-release-event' => sub {
			my($widget,$event) = @_;
			my $button = $event->button;
			if($button == 3) {
				my($self, $event) = @_;
				my ($path, $column, $cell_x, $cell_y) = $EventlistWidget->get_path_at_pos ($event->x, $event->y);
				if(scalar($widget->get_selected_indices) > 0) {
					if(defined($path)) {
						$PopupWidget->popup(undef, undef, undef, undef, 0, 0);
					}
				}
			}
		});
	PopulateEventList();
}

# Purpose: Draw the main window
# Usage: DrawMainWindow();
sub DrawMainWindow {
	# ==================================================================
	# BUILD THE MAIN WINDOW
	# ==================================================================
	# Create the main window widget
	$MainWindow = Gtk2::Window->new('toplevel');
	$MainWindow->set_title($i18n->get('Day Planner'));
	$MainWindow->set_default_size ($InternalConfig{MainWin_Width},$InternalConfig{MainWin_Height});
	$MainWindow->maximize if ($InternalConfig{MainWin_Maximized});
	if($InternalConfig{MainWin_X} and $InternalConfig{MainWin_Y}) {
		$MainWindow->move($InternalConfig{MainWin_X}, $InternalConfig{MainWin_Y});
	}

	# Set the icon
	my $MainWindowIcon = DetectImage('dayplanner-48x48.png','dayplanner-32x32.png','dayplanner-24x24.png', 'dayplanner-16x16.png', 'dayplanner.png','dayplanner_HC48.png','dayplanner_HC24.png', 'dayplanner_HC16.png', );
	if ($MainWindowIcon) {
		$MainWindow->set_default_icon_from_file($MainWindowIcon);
	}

	# Make it handle closing
	$MainWindow->signal_connect('destroy' => \&QuitSub);
	$MainWindow->signal_connect('delete-event' => \&QuitSub);

	# Handle saving maximized state
	$MainWindow->signal_connect(window_state_event => sub {
		my ($widget, $event) = @_;
		$InternalConfig{MainWin_Maximized} = ($event->new_window_state & 'maximized');
	});
		

	# Create the primary VBox for use inside it
	my $PrimaryWindowVBox = Gtk2::VBox->new();
	$PrimaryWindowVBox->show();
	$MainWindow->add($PrimaryWindowVBox);

	# ==================================================================
	# MENUBAR
	# ==================================================================
	
	# Get stock values
	my $EditStock = Gtk2::Stock->lookup('gtk-edit')->{label};
	my $QuitStock = Gtk2::Stock->lookup('gtk-quit')->{label};
	my $PrefsStock = Gtk2::Stock->lookup('gtk-preferences')->{label};
	my $AboutStock = Gtk2::Stock->lookup('gtk-about')->{label};
	my $HelpStock = Gtk2::Stock->lookup('gtk-help')->{label};

	my $DeleteStock = Gtk2::Stock->lookup('gtk-delete')->{label};

	# The menu items
	my @MenuItems = (
		# Calendar menu
		[ '/' . $i18n->get('_Calendar'),						undef,			undef,			0,	'<Branch>'],
		[ '/' . $i18n->get('_Calendar') . '/' . $i18n->get('_Import...'),		'',		\&ImportData,		1,	'<StockItem>',	'gtk-open'],
		[ '/' . $i18n->get('_Calendar') . '/' . $i18n->get('_Export...'),		undef,		\&ExportData,		1,	'<StockItem>',	'gtk-save-as'],
		[ "/" . $i18n->get("_Calendar") . '/sep',					undef,			undef,			2,	'<Separator>'],
		[ "/" . $i18n->get("_Calendar") . "/$QuitStock",		'<control>Q',		\&QuitSub,		3,	'<StockItem>',	'gtk-quit'],
		# Edit menu
		[ "/$EditStock", undef,			undef,			0,	'<Branch>'],
		[ "/$EditStock/" . $i18n->get('_Add an Event...'),		'<control>A',		\&AddEvent,		1,	'<StockItem>',	'gtk-add' ],
		[ "/$EditStock/" . $i18n->get('_Edit This Event...'),	'<control>E',		\&EditEvent,		2,	'<StockItem>',	'gtk-edit' ],
		[ "/$EditStock/" . $i18n->get('_Delete this event...'),	'<control>D',		\&DeleteEvent,		3,	'<StockItem>',	'gtk-delete' ],
		[ "/$EditStock/sep",					undef,			undef,			4,	'<Separator>'],
		[ "/$EditStock/$PrefsStock" , undef,			sub { PreferencesWindow($PrefsStock) },		1,	'<StockItem>', 'gtk-preferences' ],
		# Help menu
		[ "/$HelpStock",						undef,			undef,			0,	'<Branch>' ],
		[ "/$HelpStock/" . $i18n->get('_Report a bug'), undef, \&ReportBug, 0, '<StockItem>', 'gtk-dialog-warning'],
		[ "/$HelpStock/$AboutStock" ,undef,			\&AboutBox,		0,	'<StockItem>',	'gtk-about'],
	);
	# The accelgroup to use for the menuitems
	my $Menu_AccelGroup = Gtk2::AccelGroup->new;
	$MainWindow->add_accel_group($Menu_AccelGroup);
	# The item factory (menubar) itself
	my $Menu_ItemFactory = Gtk2::ItemFactory->new('Gtk2::MenuBar', '<main>', $Menu_AccelGroup);
	# Tell the item factory to use the items defined in @MenuItems
	$Menu_ItemFactory->create_items (undef, @MenuItems);
	# Pack it onto the vbox
	$PrimaryWindowVBox->pack_start($Menu_ItemFactory->get_widget('<main>'), 0, 0, 0);
	# Show it
	$Menu_ItemFactory->get_widget('<main>')->show();
	
	# Create two widget objects for the edit/delete menu entries
	my $Get = "/$EditStock/" . $i18n->get('_Edit This Event...');
	$Get =~ s/_//g;
	$MenuEditEntry = $Menu_ItemFactory->get_widget($Get);

	$Get = "/$EditStock/" . $i18n->get('_Delete this event...');
	$Get =~ s/_//g;
	$MenuDeleteEntry = $Menu_ItemFactory->get_widget($Get);

	# ==================================================================
	# WORKING AREA
	# ==================================================================
	# Create the hbox which will contain the rest of the program
	$WorkingAreaHBox = Gtk2::HBox->new();
	$WorkingAreaHBox->show();
	# Add it to the primary VBox
	$PrimaryWindowVBox->pack_start($WorkingAreaHBox,1,1,0);
	
	# ==================================================================
	# THE RIGHT HAND AREA
	# ==================================================================
	
	# Create the vbox for use in it
	my $RightHandVBox = Gtk2::VBox->new();
	$WorkingAreaHBox->pack_end($RightHandVBox,0,0,0);
	$RightHandVBox->show();

	# CALENDAR
	# Get the current time
	my ($currsec,$currmin,$currhour,$currmday,$currmonth,$curryear,$currwday,$curryday,$currisdst) = GetDate();
	# Create the calendar
	$CalendarWidget = Gtk2::Calendar->new;
	SetActiveCalItems($curryear, $currmonth);
	$CalendarWidget->show();
	$CalendarWidget->display_options(['show-week-numbers', 'show-day-names','show-heading']);
	$currmonth--;
	# Work around a possible Gtk2::Calendar bug by explicitly setting the month/year combo
	$CalendarWidget->select_month($currmonth, $curryear);
	$RightHandVBox->pack_start($CalendarWidget,0,0,0);
	
	my $LastDay = Get_DateInfo($CalendarWidget);

	$CalendarWidget->signal_connect('prev-month' => \&CalendarChange);
	$CalendarWidget->signal_connect('next-month' => \&CalendarChange);
	$CalendarWidget->signal_connect('prev-year' => \&CalendarChange);
	$CalendarWidget->signal_connect('next-year' => \&CalendarChange);
	$CalendarWidget->signal_connect('day-selected' => sub {
			if(Get_DateInfo($CalendarWidget) eq $LastDay) {
				return;
			}
			$LastDay = Get_DateInfo($CalendarWidget);
			PopulateEventList();
		});

	# UPCOMING EVENTS
	# Create the scrolled window
	my $UpcomingEventsWindow = Gtk2::ScrolledWindow->new;
	$UpcomingEventsWindow->set_policy('automatic', 'automatic');
	$UpcomingEventsWindow->show();
	# Create the TextView and TextBuffer objects
	$UpcomingEventsWidget = Gtk2::TextView->new();
	$UpcomingEventsBuffer = Gtk2::TextBuffer->new();
	$UpcomingEventsWidget->set_buffer($UpcomingEventsBuffer);
	$UpcomingEventsWidget->set_editable(0);
	$UpcomingEventsWidget->set_wrap_mode('word');
	$UpcomingEventsWidget->show();
	$UpcomingEventsWindow->add($UpcomingEventsWidget);
	# Pack it onto the main window
	$RightHandVBox->pack_end($UpcomingEventsWindow,1,1,0);
	
	# ==================================================================
	# LEFT HAND AREA
	# ==================================================================
	
	# Create the vbox for use in it
	my $LeftHandVBox = Gtk2::VBox->new();
	$WorkingAreaHBox->pack_start($LeftHandVBox,1,1,0);
	$LeftHandVBox->show();
	
	# Add a window for use for it
	$EventlistWin = Gtk2::ScrolledWindow->new;
	$EventlistWin->set_policy('automatic', 'automatic');
	$LeftHandVBox->pack_start($EventlistWin,1,1,0);
	$EventlistWin->show();

	# ==================================================================
	# TOOLBAR
	# ==================================================================
	
	# Tooltips
	my $Tooltips = Gtk2::Tooltips->new();
	$Tooltips->enable();
	
	# Toolbar
	$Toolbar = Gtk2::Toolbar->new();
	$Toolbar->set_style('icons');
	$LeftHandVBox->pack_end($Toolbar,0,0,0);
	$Toolbar->show();
	
	# Delete button
	$ToolbarDeleteButton = Gtk2::ToolButton->new_from_stock('gtk-delete');
	$ToolbarDeleteButton->set_tooltip($Tooltips,$i18n->get('Delete the selected event'),'');
	$Tooltips->set_tip($ToolbarDeleteButton,$i18n->get('Delete the selected event'));
	$ToolbarDeleteButton->signal_connect('clicked' => \&DeleteEvent);
	$Toolbar->insert($ToolbarDeleteButton,0);
	$ToolbarDeleteButton->show();

	# Edit button
	$ToolbarEditButton = Gtk2::ToolButton->new_from_stock('gtk-edit');
	$ToolbarEditButton->set_tooltip($Tooltips,$i18n->get('Edit the selected event'),'');
	$Tooltips->set_tip($ToolbarEditButton,$i18n->get('Edit the selected event'));
	$ToolbarEditButton->signal_connect('clicked' => \&EditEvent);
	$Toolbar->insert($ToolbarEditButton,0);
	$ToolbarEditButton->show();
	
	# Add button
	my $AddButton = Gtk2::ToolButton->new_from_stock('gtk-add');
	$AddButton->signal_connect('clicked' => \&AddEvent);
	$Toolbar->insert($AddButton,0);
	$AddButton->show();
	$AddButton->set_tooltip($Tooltips,$i18n->get('Add a new event'),'');
	$Tooltips->set_tip($AddButton,$i18n->get('Add a new event'));

	$Toolbar->get_nth_item(0)->set_is_important(1);
	$Toolbar->get_nth_item(1)->set_is_important(1);
	$Toolbar->set_tooltips(1);
	
	# Draw the initial eventlist
	DrawEventlist();
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Initialization
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Call the main data initialization functions
# Usage: MainInit();
sub MainInit {
	my $NoGui = shift;
	my $FirstStartup;
	# Set SaveToDir if it isn't already set
	if(not defined($SaveToDir)) {
		$SaveToDir = DetectConfDir();
	}
	# Set HolidayFile
	$HolidayFile = "$SaveToDir/holidays";					# The file to load holiday definitions from

	unless(-d $SaveToDir) {
		$FirstStartup =	FirstStartup($NoGui);
	}

	# If we're not in GUI mode, load the configuration file.
	# We load it later if in GUI mode in order to give the illusion of the
	# app starting faster
	if($NoGui) {
		LoadConfig();
	}
	# Load the internal state file
	LoadStateFile();
	# Set up holidays
	HolidaySetup();
	# Load the calendar
	LoadCalendar();
	return($FirstStartup);
}

# Purpose: Initialize the core program
# Usage: Gtk2->init_add(\&InitDPCore);
sub InitDPCore {
	DP_InitI18n();
	if(not defined($ENV{DP_DISABLE_EXCEPTIONSHANDLER}) or $ENV{DP_DISABLE_EXCEPTIONSHANDLER}) {
		my $Exception =  Glib->install_exception_handler(sub {
			my $Exception = shift;
			my $Chomped = $Exception;
			chomp($Chomped);
			# This is the DP perl exception handler
			DPIntWarn("An exception occurred: $Chomped");
			# Display an error
			DPError("An exception occurred. This reflects a bug in the application.\nPlease save the following exception text and report this problem to the Day Planner developers.\nNow some things may not work as expected\n\nException:\n$Exception");
			# Make the main window sensitive - this so that the program is at least
			# partially usable after the exception occurred.
			$MainWindow->set_sensitive(1);
			1;
		});
	}
	
	# Flush filehandles faster if requested
	if(defined($ENV{DP_FH_FORCEFLUSH}) and $ENV{DP_FH_FORCEFLUSH}) {
		$| = 1;
	}

	# MainInit() returns 1 if we're in "FirstStartup" mode AND are displaying the import window.
	# If we're not displaying the import window it returns 0 in any case.
	my $FirstStartup = MainInit();
	# Create the socket
	$IPC_Socket = DP::GeneralHelpers::IPC->new_server("$SaveToDir/ipcsocket",\&IPC_Handler);
	if(not $IPC_Socket) {
		my $Client = DP::GeneralHelpers::IPC->new_client("$SaveToDir/ipcsocket",sub { return });
		if(@ARGV) {
			foreach(@ARGV) {
				$Client->client_send("IMPORT_DATA $_");
			}
			exit(0);
		}
		AlreadyRunningDP($Client);
		$IPC_Socket = DP::GeneralHelpers::IPC->new_server("$SaveToDir/ipcsocket",\&IPC_Handler);
	}
	# Draw the main window
	DrawMainWindow();
	$MainWindow->show() if not $FirstStartup;
	Gtk2->main_iteration while Gtk2->events_pending;
	# Load the config
	LoadConfig();
	# Import data as told on the commandline
	if(@ARGV) {
		foreach(@ARGV) {
			IPC_Handler("IMPORT_DATA $_");
		}
	}
	# Connect to a DPS server and sync the local data with it if it's enabled
	DPS_Perform('SYNC');
	# Initialize the daemon
	DaemonInit() or die();
	# Add autostart if needed
	unless(defined($InternalConfig{AddedAutostart}) and $InternalConfig{AddedAutostart} eq "1") {
		DP_AddAutostart();
		$InternalConfig{AddedAutostart} = 1;
	}
	# Set the redraw timer and populate the upcoming events widget
	PopulateUpcomingEvents();
	Set_DayChangeTimer();
}

# Purpose: Detect the user config  directory
# Usage: DetectConfDir();
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}) {
				DP_InitI18n();
				print($i18n->get_advanced("The home directory of the user %(user) doesn't exist at %(path)! Please verify that the environment variable %(VAR) is properly set. Unable to continue\n", { user => [getpwuid($<)]->[0], path => $ENV{HOME}, VAR => 'HOME'}));
				Gtk2Init();
				DPError($i18n->get_advanced("The home directory of the user %(user) doesn't exist at %(path)! Please verify that the environment variable %(VAR) is properly set. Unable to continue\n", { user => [getpwuid($<)]->[0], path => $ENV{HOME}, VAR => 'HOME'}));
				die("\n");
			}
			$XDG_CONFIG_HOME = "$ENV{HOME}/.config";
		} else {
			Gtk2Init();
			DPError($i18n->get_advanced("The environment variable %(VAR) is not set! Unable to continue\n", { VAR => 'HOME'}));
			die($i18n->get_advanced("The environment variable %(VAR) is not set! Unable to continue\n", { VAR => 'HOME'}));
		}
	}
	return("$XDG_CONFIG_HOME/dayplanner");
}

# Get commandline options
GetOptions (
	'help|h' => sub {
		print "Day Planner version $Version\n\n";
		PrintHelp('-h','--help','Display this help screen and exit');
		PrintHelp('-v','--version', 'Display version information and exit');
		PrintHelp('-t','--test','Use a seperate debug/ configuration directory');
		PrintHelp('', '--confdir', 'Use the directory supplied as configuration directory');
		PrintHelp('-s', '--shutdaemon', 'Shutdown the daemon when Day Planner is closed');
		PrintHelp('', '--exportical', 'Export the Day Planner data in the iCalendar format');
		PrintHelp('', '', 'to the filename supplied');
		PrintHelp('', '--exporthtml', 'Export the Day Planner data in the HTML format to the');
		PrintHelp('', '', 'directory supplied');
		PrintHelp('', '--importical', 'Import data from the iCalendar file specified');
		PrintHelp('','--debuginfo', 'Display information useful for debugging and exit');
		exit(0);
	},
	'version|v' => sub {
		print "Day Planner version $Version \"$VersionName\"\n";
		print "RCS revision: $RCSRev\n";
		exit(0);
	},
	'debuginfo' => sub {
		$| = 1;
		$SaveToDir = DetectConfDir();
		print "Day Planner version $Version - $RCSRev\n";
		print "Using config dir: $SaveToDir\n";
		printf "Perl version %vd\n", $^V;
		print 'Gtk2 version ', join ('.', Gtk2->GET_VERSION_INFO),"\n";
		print 'OS: ', GetDistVer(), "\n";
		# Display module information (also includes daemon and notifier deps)
		my @AvailModules;
		my @UnavailModules;
		print "Available modules:";
		my $Prev;
		foreach my $Module (sort qw/Locale::gettext POSIX Gtk2 Data::Dumper Gtk2::SimpleList Gtk2::Gdk::Keysyms Getopt::Long Cwd File::Basename IO::Socket File::Copy FindBin MIME::Base64 Digest::MD5 File::Path Date::HolidayParser IO::Select DP::iCalendar DP::GeneralHelpers IO::Socket::SSL IPC::Open2 Net::DBus/) {
			if(eval("use $Module; 1")) {
				if($Prev) {
					print ',';
				} else {
					$Prev = 1;
				}
				my $ModVer = eval("if(defined(\$$Module\:\:VERSION)) { return \$$Module\:\:VERSION}");
				if($ModVer) {
					$ModVer =~ s/_//g;	# Some version numbers use the _ seperator inside numbers
								# printf() doesn't like it so we remove it.
					print " $Module $ModVer";
				} else {
					print " $Module";
				}
			} else {
				push(@UnavailModules, $Module);
			}
		}
		print "\nUnavailable modules:";
		print " $_" foreach(@UnavailModules);
		print ' (none)' unless(@UnavailModules);
		print "\n";
		print '@INC:';
		$Prev = false;
		foreach my $i (@INC) {
			if ($Prev)
			{
				print ',';
			}
			else
			{
				$Prev = true;
			}
			print " '".$i."'";
		}
		print "\n";
		exit(0);
	},
	'test|t:i' => sub {
		my $NumberPrefix = $_[1] ? $_[1] : '';
		$SaveToDir = DetectConfDir();
		$SaveToDir .= "/debug$NumberPrefix";
		my $Dir = $SaveToDir;
		$Dir =~ s/^$ENV{HOME}/~/g;
		DPIntInfo("Running in test mode (using $Dir)");
	},
	'confdir=s' => sub {
		if (-e $_[1]) {
			unless (-d $_[1]) {
				die "$_[1] is not a directory\n";
			}
			unless (-w $_[1]) {
				die "$_[1] is not writeable\n";
			}
		}
		$SaveToDir = $_[1];
	},
	's|shutdaemon' => \$ShutdownDaemon,
	# The export functions that call CLI_Export() needs to be seperate because
	# CLI_Export does some magic on the commandline parameter name and gets
	# confused when it gets all the options available.
	'exportical|exportics=s' => \&CLI_Export,
	'exportics_dps=s' => sub {
		CLI_Export(@_);
	},
	'exporthtml=s' => \&CLI_Export,
	'exportphp=s' => \&CLI_Export,
	'importical|importics=s' => \&CLI_Import,
) or die "Run $0 --help for more information\n";

my $Gtk2_Initialized = 0;

Gtk2Init();
InitDPCore();
# Rest in the Gtk2 main loop
Gtk2->main;
