#!/usr/bin/perl
# Day Planner notifier
# Sends a notification to the user
# Copyright (C) Eskild Hustvedt 2006, 2007
# $Id: dayplanner-notifier 1870 2008-01-04 13:30:31Z 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/>.

use strict;			# Force strict coding
use warnings;			# Tell perl to display warnings
use Gtk2;			# Use Gtk2
use Getopt::Long;		# Commandline options
use POSIX;			# setlocale()
use FindBin;
# These constants prettify things
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/";
use lib "$FindBin::RealBin/modules/Date-HolidayParser/lib/";
use lib "$FindBin::RealBin/modules/DP-iCalendar/lib/";
use lib "$FindBin::RealBin/modules/DP-GeneralHelpers/lib/";
# Day Planner-specific libs
use DP::GeneralHelpers qw(AppendZero DPIntWarn);
use DP::GeneralHelpers::I18N;
use DP::iCalendar qw(iCal_ParseDateTime iCal_ConvertToUnixTime);

my $RealZero = $0;
$0 = 'dayplanner-notifier';
my $Gettext;			# Global Gettext object
my $Version = '0.8.1';
my $RCSRev = '$Id: dayplanner-notifier 1870 2008-01-04 13:30:31Z zero_dogg $';

my $CalendarPath;		# The source iCalendar file
my $DP_I18N_Mode;
my $Message;			# The message
my $Fulltext;			# The fulltext (details)
my $Time;			# The time
my $Date;			# The date
my $IsWarning;			# If it is a warning or the actual event time
my $i18n;			# The DP::GeneralHelpers::I18N object

my $MessageID;			# The daemon message ID
my $NoFork;
my $LaunchTime = time();

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

# 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: Create the details widget
# Usage: my $ExpanderWidget = CreateDetailsWidget();
sub CreateDetailsWidget {
	my $FT_Expander = Gtk2::Expander->new($i18n->get('Show details'));
	$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 details'));
			} else {
				$FT_Expander->set_label($i18n->get('Show details'));
			}
		});
	return($FT_Expander);
}

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

# Purpose: Test if X is available
# Usage: if(X_Available()) { Available } else { Not available }
sub X_Available {
	# Check if we've got a gtk2 version above 2.10.0, if we do then we
	# use the builtin Gtk2->init_check function. If not, we fall back to using
	# the uglier, slower and less portable system() test
	#
	# We can't eval() or similar the ->init function because it dies at library (C)
	# level, which is below perl and can't be caught and handled properly.
	if(Gtk2->CHECK_VERSION(2,10,0)) {
		# Use the Gtk2->init_check function
		if(Gtk2->init_check()) {
			return(1);
		} else {
			return(0);
		}
	} else {
		# Fall back to using the ugly system()-based test.

		# This statement might look a bit weird, but remember that for commandline
		# programs 0 is true and anything else is false.
		if(system('perl', '-e', 'use Gtk2; open(STDERR, \'>/dev/null\'); Gtk2->init;')) {
			return(0);
		} else {
			return(1);
		}
	}
}

# 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;
}

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

# Purpose: Get information from the calendar
# Usage: GetMessagesFromCalendar(Calendar, UID);
sub GetMessagesFromCalendar
{
	my $calendar = shift;
	my $UID = shift;
	my $iCalendar = DP::iCalendar->new($calendar);
	my $UIDInf = $iCalendar->get_info($UID);
	$Message = $UIDInf->{'SUMMARY'};
	$Fulltext = $UIDInf->{'DESCRIPTION'};
	$IsWarning = true;	# Default to warning
	$Date = 'tomorrow'; # Default to tomorrow
	my ($eventYear, $eventMonth, $eventDay, $eventTime) = iCal_ParseDateTime($UIDInf->{DTSTART});
	$Time = $eventTime;
	# Find out if the UID is now or not
	#  We try both launch time and launch time plus/minus 60 seconds. This so that we
	#  get reliable information even during high system loads and in the offchance that
	#  something is not right.
	foreach my $possibleTime (@{[$LaunchTime, $LaunchTime - 60, $LaunchTime + 60]})
	{
		my ($sec,$min,$hour,$mday,$month,$year,$wday,$yday,$isdst) = GetDate($possibleTime);
		if($iCalendar->UID_exists_at($UID,$year,$month,$mday,AppendZero($hour).':'.AppendZero($min)))
		{
			$IsWarning = false;
		}
		elsif(iCal_ConvertToUnixTime($UIDInf->{DTSTART}) <= time())
		{
			$IsWarning = false;
		}
		if($iCalendar->UID_exists_at($UID,$year,$month,$mday))
		{
			$Date = 'today';
		}
	}
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# MAIN NOTIFICATION
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Find out how to notify the user and do it
# Usage: NotifyUser();
sub NotifyUser {
	$i18n = DP::GeneralHelpers::I18N->new();
	$Time = $i18n->AMPM_From24($Time);	# Convert to AM/PM if needed
	if(defined($ENV{DISPLAY}) and length($ENV{DISPLAY}) and X_Available()) {
		# We have DISPLAY, use GTK
		GtkNotifier();
	} else {
		NonGUINotifier();
	}
}

# Purpose: Notify the user using a graphical (gtk2) dialog
# Usage: GtkNotifier();
sub GtkNotifier {
	Gtk2->init;
	while (1) {
		my $MainText;
		if($Date eq 'today') {
			$MainText = $i18n->get_advanced("Today at %(time):\n%(event_description)", { 'time' => $Time, 'event_description' => $Message});
		} elsif ($Date eq 'tomorrow') {
			$MainText = $i18n->get_advanced("Tomorrow at %(time):\n%(event_description)", { 'time' => $Time, 'event_description' => $Message});
		} else {
			$MainText = $i18n->get_advanced("At %(time) on %(date):\n%(event_description)", { 'time' => $Time, date => $Date, event_description => $Message});
		}
		# If it is a warning then we use the "info" type.
		# If it isn't a warning, then we use the "warning" type to attempt to
		#  display the urgency of the notification better.
		my $DialogType = $IsWarning ? 'info' : 'warning';
		my $NotifyDialog = Gtk2::MessageDialog->new (undef,
							'destroy-with-parent',
							$DialogType,
							'none',
							$MainText);
		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) {
			$NotifyDialog->set_default_icon_from_file($WindowIcon);
		}
		# This is important, so set the urgency hint and keep it above all other windows
		# and on all desktops
		$NotifyDialog->set_keep_above(1);
		$NotifyDialog->stick;
		$NotifyDialog->set_title($i18n->get('Day Planner event'));
		$NotifyDialog->set_skip_pager_hint(0);
		$NotifyDialog->set_skip_taskbar_hint(0);
		$NotifyDialog->set_default_size(300,-1);
		$NotifyDialog->set_type_hint('dialog');
		$NotifyDialog->set_deletable(0);
		if(Gtk2->CHECK_VERSION(2,10,0)) {
			$NotifyDialog->set_urgency_hint(1);
		}
		
		my $Tooltips = Gtk2::Tooltips->new();
		my $PostponeButton = Gtk2::Button->new_with_label($i18n->get('Remind me later'));
		$PostponeButton->can_default(1);
		my $PostponeImage = Gtk2::Image->new_from_stock('gtk-go-forward','button');
		$PostponeButton->set_image($PostponeImage);
		$NotifyDialog->add_action_widget($PostponeButton, 'reject');
		$PostponeButton->show();
		$Tooltips->enable();
		$Tooltips->set_tip($PostponeButton, $i18n->get('Postpone this notification for 10 minutes'));
	
		my $OkayButton = Gtk2::Button->new_from_stock('gtk-ok');
		$OkayButton->can_default(1);
		$NotifyDialog->add_action_widget($OkayButton, 'accept');
		$OkayButton->show();

		$NotifyDialog->set_default_response('reject');
		
		# If the $Fulltext is defined and set then allow the user to be shown it
		if(defined($Fulltext) and $Fulltext =~ /\S/) {
			# The expander
			my $FT_Expander = CreateDetailsWidget();;
			$NotifyDialog->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($Fulltext);
			$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);
		}
		# Display, get the reply and destroy (this sounds a bit violent - I assure you no widgets will be hurt)
		my $GtkReply = $NotifyDialog->run;
		$NotifyDialog->destroy();
		# Flush the display before sleeping
		Gtk2->main_iteration while Gtk2->events_pending;
		if($GtkReply eq 'reject') {
			sleep(60*10);
		} else {
			return(1);
		}
	}
}

# Purpose: Notify the user using a nongraphical system
# Usage: NonGUINotifier();
sub NonGUINotifier {
	my ($sysname, $nodename, $release, $version, $machine) = POSIX::uname();
	unless($sysname eq 'Linux') {
		print "*** dayplanner-notifier ($Version): Not running under GNU/Linux but attempting to use the NonGUINotifier. This might not work as expected if a non-GNU compatible who is installed\n";
	}
	# Discussion follows
	#
	# To further up the ability of the daemon to use the GtkNotifier, perhaps the dayplanner
	# process could use a new HI format: HI client $ENV{DISPLAY} so that the daemon can set
	# its own DISPLAY variable more properly - that is, set it to the DISPLAY variable of
	# the last client started. Maybe rather add a command "ADD_DISPLAY" to the daemon.
	# Perhaps also a dayplanner-daemon --add-display command, which
	# adds the current DISPLAY or the display supplied to the display pool
	my $MainText;
	if($Date eq 'today') {
		$MainText = $i18n->get_advanced("Today at %(time):\n%(event_description)", { 'time' => $Time, 'event_description' => $Message});
	} elsif ($Date eq 'tomorrow') {
		$MainText = $i18n->get_advanced("Tomorrow at %(time):\n%(event_description)", { 'time' => $Time, 'event_description' => $Message});
	} else {
		$MainText = $i18n->get_advanced("At %(time) on %(date):\n%(event_description)", { 'time' => $Time, date => $Date, event_description => $Message});
	}
	unless(WriteTo(cuserid(), $MainText)) {
		print "---\n$MainText\n---\n";
	}
}

# Purpose: Write a message to all writable terminals owned by the user supplied
# Usage: my $Return = WriteTo(USER, MESSAGE);
# 	The return value is 0 if nothing was written anywhere, 1 if something was
# 	written.
sub WriteTo {
	my ($User,$Message) = @_;
	return(0) unless InPath('who');
	return(0) unless InPath('write');
	my $Return = 0;
	open(my $WHO, 'who -T|');
	my @WritableDevs;
	while(<$WHO>) {
		next unless s/^$User\s+//;
		chomp;
		my $Writable;

		if(s/^\+\s+//) {
			s/^(\S+).+/$1/;
			push(@WritableDevs, $_);
		}
	}
	close($WHO);
	foreach my $Target (@WritableDevs) {
		open(my $WRITE, "|write $User $Target");
		print $WRITE "$Message";
		close($WRITE);
		$Return = 1;
	}
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# INITIALIZATION
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
GetOptions (
	'help|h' => sub {
		print "Day Planner notifier version $Version\n";
		print "This program is for use by the dayplanner daemon.\n\n";
		print "Use a --time/--message/--date (and --fulltext) combo if you want to call it manually,\n";
		print "the daemon uses --uid and --calendar.\n\n";
		PrintHelp('-h','--help','Display this help screen');
		PrintHelp('-v', '--version', 'Display version information and exit');
		PrintHelp('-m', '--message', 'Set the message');
		PrintHelp('-f', '--fulltext', 'Set the "fulltext" entry (details)');
		PrintHelp('-t', '--time', 'Set the time (HH:MM)');
		PrintHelp('-d', '--date', 'Set the date (a date string, "today" or "tomorrow")');
		PrintHelp('-c', '--calendar', 'Set the iCalendar file');
		PrintHelp('', '--uid', 'Set the iCalendar UID to display');
		PrintHelp('-n', '--nofork', 'Don\'t go into the background');
		exit(0);
	},
	'message|m=s' => \$Message,
	'time|t=s' => \$Time,
	'fulltext|f=s' => \$Fulltext,
	'date|d=s' => \$Date,
	'uid=s' => \$MessageID,
	'n|nofork' => \$NoFork,
	'c|calendar=s' => \$CalendarPath,
	'v|version' => sub {
		print "Day Planner notifier version $Version\n";
		print "RCS revision: $RCSRev\n";
		exit(0);
	},
	# For backwards compatibility
	's|i|id|socket' => sub
	{
		print "$_[0]: deprecated.\n";
		# Exit with a nonzero return value - the daemon will then continue to look for another notifier
		exit(1);
	},
) or die "See $0 --help for more information\n";

# Verify various things
unless(defined($MessageID) and length($MessageID)) {
	die "I need a --time\n" unless defined($Time) and length($Time);
	die "I need a --message\n" unless defined($Message) and length($Message);
	die "I need a --date\n" unless defined($Date) and length($Date);
}

if(defined($MessageID)) {
	die("Needs a --calendar\n") if not $CalendarPath;
	die("$CalendarPath: does not exist\n") if not -e $CalendarPath;
	die("$CalendarPath: is not readable\n") if not -r $CalendarPath;
}

unless($NoFork) {
	# Okay, we're here now, fork
	my $PID = fork;
	exit if $PID;
	die "I was unable to fork: $!\n" unless defined($PID);
}

if(defined($MessageID)) {
	GetMessagesFromCalendar($CalendarPath,$MessageID);
}

NotifyUser();
