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

use strict;			# Force strict coding
use warnings;			# Tell perl to display warnings
use Gtk2;			# Use Gtk2
use Getopt::Long;		# Commandline options
use IO::Socket;			# Network layer to communicate with the daemon
use Locale::gettext;		# Translation layer
use POSIX;			# setlocale()
use Cwd;
use File::Basename;

my $Gettext = Locale::gettext->domain("dayplanner");	# Set the gettext domain
setlocale(LC_ALL, "" );
textdomain("dayplanner");

my $Version = "0.2";
my $RCSRev = '$Id: dayplanner-notifier 466 2006-07-31 18:08:14Z zero_dogg $';
my $SoftwareDir = dirname(Cwd::realpath($0));

my $SocketName;			# The socket to connect to
my $Socket;			# The IO::Socket object

my $Message;			# The message
my $Fulltext;			# The fulltext (details)
my $Time;			# The time
my $Date;			# The date

my $MessageID;			# The daemon message ID
my $NoFork;

# Find out if we have a locale directory in our main dir
if (-d "$SoftwareDir/locale/" and defined($ENV{LANG})) {
	my $I18N = $ENV{LANG};
	if($I18N =~ /:/) {
		$I18N =~ s/^(.+):.*$/$1/;
	}
	if (-e "$SoftwareDir/locale/$I18N/LC_MESSAGES/dayplanner.mo") {
		bindtextdomain("dayplanner", "$SoftwareDir/locale");
	}
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# DAEMON COMMUNICATION
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

# Purpose: Connect to the daemon
# Usage: DaemonConnect();
sub DaemonConnect {
	$Socket = IO::Socket::UNIX->new(Peer => $SocketName,
					Type => SOCK_STREAM,
					Timeout => 10 )
					or DPIntWarn("Unable to connect to $SocketName: $@!") and return(0);
	if(Daemon_DataSegment("HI servant") eq "HI") {
		return(1);
	} else {
		DPIntWarn("Unable to connect to $SocketName: The daemon refused my connection\n");
		close($Socket);
		return("NOHI");
	}

}

# Purpose: Send some data to the daemon
# Usage: Daemon_SendData(DATA);
sub Daemon_SendData {
	print $Socket "$$ $_[0]","\n" or DPIntWarn("Problem sending data to the daemon: $!") and return(0);
}

# Purpose: Get some data from the daemon
# Usage: $Data = Daemon_GetData();
sub Daemon_GetData {
	my $Data = <$Socket>;
	unless(defined($Data)) {
		return("ERR UNABLE_TO_GET_DATA");
	}
	if ($Data =~ /^BEGIN MULTILINE/) {
		return(UnwrapMultiLinePacket());
	} else {
		chomp($Data);
		return($Data);
	}
}

# Purpose: Send something to the daemon and then return the daemons reply
# Usage: $Reply = Daemon_DataSegment(DATA);
sub Daemon_DataSegment {
	Daemon_SendData($_[0]);
	return(Daemon_GetData());
}

# Purpose: Disconnect from the daemon
# Usage: DaemonDisconnect();
sub DaemonDisconnect {
	Daemon_DataSegment("BYE");
	close($Socket);
}

# Purpose: Unwrap multiline packets correctly
# Usage: my $Multiline = UnwrapMultiLinePacket();
sub UnwrapMultiLinePacket {
	my $MultiLineNotEnded = 1;
	my $NewMessage;
	my $UnhandledLimit;
	my $DidSomething;
	my $DidNotDoAnything;
	while ($MultiLineNotEnded) {
		$DidSomething = 0;
		foreach(split(/\n/,<$Socket>)) {
			$DidSomething = 1;
			if (/^END MULTILINE/) {
				$MultiLineNotEnded = 0;
				last;
			}elsif (s/^\s*ML //) {
				chomp;
				if(defined($NewMessage)) {
					$NewMessage = $NewMessage . "$_\n";
				} else {
					$NewMessage = "$_\n";
				}
			} elsif (/^NNL$/) {
				# This means the preceeding line doesn't end with \n
				unless(defined($NewMessage)) {
					DPIntWarn("Recieved NNL while unwrapping a multiline packet without any preceeding line. This is a bug in either the daemon or notifier. Ignoring request and proceeding");
					next;
				}	
				$NewMessage =~ s/\n$//;
			} else {
				chomp($_);
				DPIntWarn("Unhandled daemon command: $_\n");
				$UnhandledLimit++;
			}
			if(defined($UnhandledLimit) and $UnhandledLimit >= 20) {
				DPIntWarn("Limit of 20 unhandled daemon commands within UnwrapMultiLinePacket exceeded. Assuming END MULTILINE");
				$MultiLineNotEnded = 0;
				last;
			}
		}
		if($DidSomething == 0) {
			$DidNotDoAnything++;
		} else {
			$DidNotDoAnything = 0;
		}
		if(defined($DidNotDoAnything) and $DidNotDoAnything >= 20) {
			DPIntWarn("Limit of 20 idle turns through the UnwrapMultiLinePacket loop exceeded. Assuming END MULTILINE");
			$MultiLineNotEnded = 0;
			last;
		}
	}
	unless(defined($NewMessage)) {
		DPIntWarn("\$NewMessage was undefined while unwrapping a multiline packet. This is probably a bug. Returning \"\" scalar string value to caller");
		return("");
	}
	unless($NewMessage =~ /\n.*\n/) {
		chomp($NewMessage);
	}
	return($NewMessage);
}

# Purpose: Get the messages from the daemon
# Usage: GetMessages(Message ID);
sub GetMessages {
	unless(Daemon_DataSegment("NOTIFICATION VERIFY $_[0]") eq 'SET') {
		die("The MessageID $_[0] is invalid according to the daemon\n");
	}
	
	$Message = Daemon_DataSegment("NOTIFICATION GET $_[0] Summary");
	$Time = Daemon_DataSegment("NOTIFICATION GET $_[0] Time");
	$Date = Daemon_DataSegment("NOTIFICATION GET $_[0] Date");
	$Fulltext = Daemon_DataSegment("NOTIFICATION GET $_[0] Fulltext");

	# Fulltext is a multiline packet and can thus return "" on failure.
	# We assume $_[0] UNDEF on ""/failure
	if($Fulltext eq "$_[0] UNDEF" or $Fulltext eq "") {
		$Fulltext = undef;
	}

	Daemon_DataSegment("NOTIFICATION CLEAN $_[0]");
}

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

# Purpose: Display a nicely formatted warning
# Usage: DPIntWarn("Warning");
sub DPIntWarn {
	warn "*** (Day Planner notifier $Version) Warning: $_[0]\n";
}

# 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 {
	# This might look a bit weird, but remember that for commandline programs 0 is true
	# and anything else is false.
	#
	# The reason we use a seperate perl instance is becuase Gtk2 dies at *C level*
	# (below perl) so we can't trap it with eval.
	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;
}


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

# Purpose: Find out how to notify the user and do it
# Usage: NotifyUser();
sub NotifyUser {
	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 = sprintf($Gettext->get("Today at %s:\n%s"),$Time,$Message);
		} elsif ($Date eq 'tomorrow') {
			$MainText = sprintf($Gettext->get("Tomorrow at %s:\n%s"),$Time,$Message);
		} else {
			$MainText = sprintf($Gettext->get("At %s on %s:\n%s"),$Time,$Date,$Message);
		}
		my $NotifyDialog = Gtk2::MessageDialog->new (undef,
							'destroy-with-parent',
							'info',
							'none',
							$MainText);
		# 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($Gettext->get("Day planner event"));
		
		my $PostponeButton = Gtk2::Button->new_with_label($Gettext->get('Postpone'));
		$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();
	
		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 = Gtk2::Expander->new($Gettext->get("Details"));
			$FT_Expander->show();
			$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 = sprintf($Gettext->get("Today at %s:\n%s"),$Time,$Message);
	} elsif ($Date eq 'tomorrow') {
		$MainText = sprintf($Gettext->get("Tomorrow at %s:\n%s"),$Time,$Message);
	} else {
		$MainText = sprintf($Gettext->get("At %s on %s:\n%s"),$Time,$Date,$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 --id.\n\n";
		PrintHelp("-h","--help","Display this help screen");
		PrintHelp("-v", "--version", "Display version information and exit");
		PrintHelp("-s","--socket","Select the daemon socket");
		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("-i", "--id", "Set the daemon message ID");
		PrintHelp("-n", "--nofork", "Don't go into the background");
		exit(0);
	},
	'socket|s=s' => \$SocketName,
	'message|m=s' => \$Message,
	'time|t=s' => \$Time,
	'fulltext|f=s' => \$Fulltext,
	'date|d=s' => \$Date,
	'id|i=s' => \$MessageID,
	'n|nofork' => \$NoFork,
	'v|version' => sub {
		print "Day planner notifier version $Version\n";
		print "RCS revision: $RCSRev\n";
		exit(0);
	},
) 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 "I need a --socket to connect to\n" unless defined($SocketName) and length($SocketName);
	die "$SocketName does not exist\n" unless -e $SocketName;
	die "$SocketName is not a socket\n" unless -S $SocketName;
}

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)) {
	DaemonConnect();
	GetMessages($MessageID);
}

NotifyUser();
if(defined($MessageID)) {
	DaemonDisconnect();
}
