# snapshot of /television/bot/Autobot/FingertipTV/Responder.pm
# taken 2003-02-09 to accompany user path notes

package Autobot::FingertipTV::Responder;

use strict;
use POE::Session;
use DBI;
use Time::Local;

sub _start {
	my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];

	# get variables
	$heap->{buddy_name} = $_[ARG0];
	my $message = $_[ARG1];
	
	# start the database connection
	$heap->{dbh} = DBI->connect(
		"DBI:mysql:xxxx",
		"xxxx",
		"xxxx"
	);
	
	# default channel names
	$heap->{channels} = {
		bbc1 => "BBC1",
		bbc2 => "BBC2",
		itv  => "Carlton",
		ch4  => "Ch4",
		ch5  => "Ch5"
	};

	$heap->{channelmap} = {
		bbc1 => "BBC1",
		bbc2 => "BBC2",
		itv  => "Carlton",
		ch4  => "Ch4",
		ch5  => "Ch5"
	};
	
	# set up internal cursors
	# this is updated every time there's I/O
	$heap->{cursor}->[0]->{time} = &Autobot::FingertipTV::utility::now();
	$heap->{cursor}->[0]->{channel} = undef;
	$heap->{cursor}->[0]->{programme} = 0;
	$heap->{cursor}->[0]->{options} = {};
	$heap->{cursor}->[0]->{input} = undef;
	$heap->{cursor}->[0]->{output} = undef;

	$kernel->yield('logger' => "Starting");
	$kernel->yield('in' => $message);
	
	return $heap->{buddy_name};
}

sub _stop {
	my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];

	$kernel->yield('logger' => "Stopping");

	$heap->{dbh}->disconnect;
}

sub logger {
	my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];
	
	my $log_message = $_[ARG0];
	
	print "[fingertiptv <" . $heap->{buddy_name} . ">] ";
	print $log_message;
	print "\n";
}

sub in {
	my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];
	$kernel->delay( time_is_up => 60 );
	
	my $message = $_[ARG0];
	
	$kernel->yield('logging' => "Handling <$message>");
	
	# there are various input types. figure out which one this is
	# a. now
	# b. now [channel]
	# c. later
	# d. later [channel]
	# e. earlier
	# f. earlier [channel]
	# g. next
	# h. next [channel]
	# i. previous
	# j. previous [channel]
	# k. [time]
	# l. [time] [channel]
	# m. [channel]
	# n. [programme id from list]
	# o. help

	$message =~ tr/A-Z/a-z/;

	# some variables to extract if possible
	my $channel = undef;
	my $time = undef;
	my $input = undef;
	my $option = undef;
	
	# a helper
	my $expecting_option =
		(keys %{$heap->{cursor}->[0]->{options}}) > 0 ? 1 : 0;

	# try special keywords first
	if($message =~ m/\bhelp\b/) {
		$input = "o";
	}
	elsif($message =~ m/^\s*home\s*$/) {
		$input = "a";
	}
	elsif($message =~ m/^\s*(\d+)\s*$/) {
		$option = $1;
		$input = "n";
	}
	else {
		# see if a channel can be recognised
		if($message =~ m/\b((bbc\s*[1|2])|(itv)|(ch(ann?el)?\s*4)|(ch(ann?el)?\s*5)|(r(adio)?4))\b/i) {
			$channel = $1;
			$channel =~ s/\s*//g;
			if($channel eq 'channel4') {
				$channel = "ch4";
			}
			elsif($channel eq 'channel5') {
				$channel = "ch5";
			}
			elsif($channel eq 'radio4') {
				$channel = 'r4';
			}
		}
	
		# see if a time can be extracted
		if($message =~ m/\b(((\d+)[\.:\-](\d+)\s*([a|p]m)?)|((\d+)\s*([a|p]m)))\b/) {
			my ($all, $one, $hr1, $min1, $apm1, $two, $hr2, $apm2) = ($1, $2, $3, $4, $5, $6, $7, $8);
			my ($hours, $minutes);
			if($one) {
				$hours = $hr1;
				if($apm1 && $apm1 eq 'pm' && $hours<13 && $hours >= 0) {
					$hours += 12;
				}
				$minutes = $min1;
			}
			elsif($two) {
				$hours = $hr2;
				if($apm2 && $apm2 eq 'pm' && $hours<13 && $hours >= 0) {
					$hours += 12;
				}
				$minutes = 0;
			}
			if($hours<24 && $hours>=0 && $minutes<60 && $minutes >= 0) {
				$time = Autobot::FingertipTV::utility::epochtime($hours, $minutes);
			}
		}
	
		# now see if there's a keyword that can be recognised
		if($message =~ m/\bnow\b/) {
			$input = $channel ? "b" : "a";
		}
		elsif($message =~ m/\blater\b/) {
			$input = $channel ? "d" : "c";
		}
		elsif($message =~ m/\bearlier\b/) {
			$input = $channel ? "f" : "e";
		}
		elsif($message =~ m/\bnext\b/) {
			$input = $channel ? "h" : "g";
		}
		elsif($message =~ m/\bprev(ious)?\b/) {
			$input = $channel ? "j" : "i";
		}
		elsif(defined($time)) {
			$input = $channel ? "l" : "k";
		}
		elsif(defined($channel)) {
			$input = "m";
		}
	}
	
	# so now we have an $input which is a letter or undef,
	# and we have to decide which output to do
	# the possible outputs are
	# output_list
	# output_details
	# output_medium
	# output_error
	my $cursor = {
		'time' => $heap->{cursor}->[0]->{'time'},
		'channel' => $heap->{cursor}->[0]->{'channel'},
		'programme' => $heap->{cursor}->[0]->{'programme'},
		'options' => {}, # output state responsible for this
		'input' => $input,
		'output' => undef, # MUST be populated below
		'option_chosen' => undef
	};

	my $hint_message = "";
	if(!defined($input)) {
		$cursor->{'output'} = "error";
	}
	elsif($input eq 'a') {
		$cursor->{'channel'} = undef;
		$cursor->{'time'} = &Autobot::FingertipTV::utility::now();
		$cursor->{'programme'} = 0;
		$cursor->{'output'} = "list";
		$hint_message = "On now:";
	}
	elsif($input eq 'b') {
		$cursor->{'channel'} = $channel;
		$cursor->{'time'} = &Autobot::FingertipTV::utility::now();
		$cursor->{'programme'} = 0;
		$cursor->{'output'} = "medium";
		$hint_message = "On now on " . $heap->{channels}->{ $cursor->{channel} } . ":";
	}
	elsif($input eq 'c') {
		# when doing later, need to take into account the
		# programme cursor
		$cursor->{'time'} = &Autobot::FingertipTV::utility::later( $heap->{cursor}->[0]->{'time'} );
		$cursor->{'programme'} = 0;
		if($cursor->{channel}) {
			$cursor->{'output'} = "medium";
			$hint_message = "On " . $heap->{channels}->{ $cursor->{channel} } . ":";
		}
		else {
			$cursor->{'output'} = "list";
			$hint_message = "On at " . &Autobot::FingertipTV::utility::nicetime($cursor->{'time'}) . ":";
		}
	}
	elsif($input eq 'd') {
		# when doing later, need to take into account the
		# programme cursor
		$cursor->{'channel'} = $channel;
		$cursor->{'time'} = &Autobot::FingertipTV::utility::later( $heap->{cursor}->[0]->{'time'} );
		$cursor->{'programme'} = 0;
		$cursor->{'output'} = "medium";
		$hint_message = "On " . $heap->{channels}->{ $cursor->{channel} } . ":";
	}
	elsif($input eq 'e') {
		# when doing earlier, need to take into account the
		# programme cursor
		$cursor->{'time'} = &Autobot::FingertipTV::utility::earlier( $heap->{cursor}->[0]->{'time'} );
		$cursor->{'programme'} = 0;
		if($cursor->{channel}) {
			$cursor->{'output'} = "medium";
			$hint_message = "On " . $heap->{channels}->{ $cursor->{channel} } . ":";
		}
		else {
			$cursor->{'output'} = "list";
			$hint_message = "On at " . &Autobot::FingertipTV::utility::nicetime($cursor->{'time'}) . ":";
		}
	}
	elsif($input eq 'f') {
		# when doing earlier, need to take into account the
		# programme cursor
		$cursor->{'channel'} = $channel;
		$cursor->{'time'} = &Autobot::FingertipTV::utility::earlier( $heap->{cursor}->[0]->{'time'} );
		$cursor->{'programme'} = 0;
		$cursor->{'output'} = "medium";
		$hint_message = "On " . $heap->{channels}->{ $cursor->{channel} } . ":";
	}
	elsif($input eq 'g') {
		++$cursor->{'programme'};
		if($cursor->{channel}) {
			$cursor->{'output'} = "medium";
			$hint_message = "On " . $heap->{channels}->{
				$cursor->{channel} };
			$hint_message .= $cursor->{programme} == 1
				? " next:" : ":";
		}
		else {
			$cursor->{'output'} = "list";
			$hint_message .= $cursor->{programme} == 1
				? "Next:" : "";
		}
	}
	elsif($input eq 'h') {
		$cursor->{'channel'} = $channel;
		++$cursor->{'programme'};
		$cursor->{'output'} = "medium";
		$hint_message = "On " . $heap->{channels}->{
			$cursor->{channel} };
		$hint_message .= $cursor->{programme} == 1
			? " next:" : ":";		
	}
	elsif($input eq 'i') {
		--$cursor->{'programme'};
		if($cursor->{channel}) {
			$cursor->{'output'} = "medium";
			$hint_message = "On " . $heap->{channels}->{ $cursor->{channel} };
			$hint_message .= $cursor->{programme} == -1
				? " previously:" : ":";
		}
		else {
			$cursor->{'output'} = "list";
			$hint_message .= $cursor->{programme} == -1
				? "Previously:" : "";
		}
	}
	elsif($input eq 'j') {
		$cursor->{'channel'} = $channel;
		--$cursor->{'programme'};
		$cursor->{'output'} = "medium";
		$hint_message = "On " . $heap->{channels}->{ $cursor->{channel} };
		$hint_message .= $cursor->{programme} == 1
			? " previously:" : ":";		
	}
	elsif($input eq 'k') {
		$cursor->{'time'} = $time;
		$cursor->{'programme'} = 0;
		$cursor->{'output'} = $cursor->{channel} ? "medium" : "list";
		$hint_message = "On at " . &Autobot::FingertipTV::utility::nicetime($cursor->{'time'}) . ":";
	}
	elsif($input eq 'l') {
		$cursor->{'channel'} = $channel;
		$cursor->{'time'} = $time;
		$cursor->{'programme'} = 0;
		$cursor->{'output'} = "list";
		$hint_message = "Starting at " . &Autobot::FingertipTV::utility::nicetime($cursor->{'time'}) . ":";
	}
	elsif($input eq 'm') {
		# this probably isn't the best way of doing things
		$cursor->{'channel'} = $channel;
		$cursor->{'programme'} = 0;
		$cursor->{'output'} = "list";
		$hint_message = "On " . $heap->{channels}->{$cursor->{channel}} . " at " . &Autobot::FingertipTV::utility::nicetime($cursor->{'time'}) . ":";

	}
	elsif($input eq 'n') {
		# maintain options for next time
		$cursor->{options} = $heap->{cursor}->[0]->{options};
		if(defined($heap->{cursor}->[0]->{options}->{$option})) {
			$cursor->{'option_chosen'} = $option;
			$cursor->{'output'} = "details";
		}
		else {
			$cursor->{'output'} = "error";
			$hint_message = "Please choose an option listed above.";
		}
	}
	elsif($input eq 'o') {
		$cursor->{'output'} = "error";
		$hint_message = "I can tell you what's on television today. If you say a time and/or a television channel (the four main terrestial channels only), or even just 'now', that'll do for a start.";
	}
	
	my $state = "output_" . $cursor->{output};
	unshift @{$heap->{cursor}}, $cursor;

	my $log = join(", ", ( map { $_ . ":" . ($cursor->{$_} || "") } keys %{$cursor}));
	$kernel->yield('logger' => $log);
	$kernel->yield($state => $hint_message);

}

sub output_list {
	my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];

	my $hint = $_[ARG0];

	my $programmes_per_channel =
		$heap->{cursor}->[0]->{channel}
		? 5
		: 1;

	my @results = &Autobot::FingertipTV::utility::programme_list(
		$heap->{dbh},
		$heap->{cursor}->[0],
		$programmes_per_channel,
		$heap->{channelmap} );

	# sort the results
	my $channelweight = {
	    BBC1 => 1, BBC2 => 2, Carlton => 3, Ch4 => 4, Ch5 => 5 };

	@results = sort {
	    ( $channelweight->{$a->{channel}}
	    <=>
	    $channelweight->{$b->{channel}})
		    ||
		( $a->{epoch_time} <=> $b->{epoch_time} )    
	} @results;
	

	# now we've got results in @results, and it's time to display
	my @message;
	if(@results == 0) {
		push @message, "Sorry, couldn't find any programmes.";
	}
	else {
		my $optioncount = 1;
		my $optionslist = {};
		foreach my $prog (@results) {
			my @when = gmtime($prog->{epoch_time});
			my $text = "<b>" . $prog->{channel} . "</b> at " . $when[2] . "." . $when[1] . ": " . $prog->{name};
			$text .= " (<b>$optioncount</b>)";
			push @message, $text;
			$optionslist->{$optioncount} = $prog->{id};
			++$optioncount;
		}
		unshift @message, $hint;
		push @message, "<i>Ask again, choose a programme, or see </i>earlier<i> or </i>later<i> times.</i>";
		$heap->{cursor}->[0]->{options} = $optionslist;
	}
	
	my $message = join("<br>", @message);
	
	$kernel->post("tele", "send", $heap->{buddy_name}, $message);
			
}

sub output_medium {
	my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];

	my $hint = $_[ARG0];
	
	# - dbh
	# - cursor
	# - how many programmes to fetch per channel
	# - the db channelmap
	my @results = &Autobot::FingertipTV::utility::programme_list(
		$heap->{dbh},
		$heap->{cursor}->[0],
		2,
		$heap->{channelmap} );
	
	# now we've got the reuslts in @results, time to display
	my @message;
	if(@results == 0) {
		push @message, "Sorry, couldn't find any programmes.";
	}
	else {
		my $optioncount = 1;
		my $optionslist = {};
		foreach my $prog (@results) {
			my @when = gmtime($prog->{epoch_time});
			my $text;
			if($optioncount == 1) {
				$text = "At " . $when[2] . "." . $when[1] . ", <b>" . $prog->{name} . "</b>";
			    my $prog = &Autobot::FingertipTV::utility::programme_details(
					$heap->{dbh},
					$prog->{id} );
				my $details = $prog->{level0};
				if($details) {
					$text .= ": <i>$details</i>";
				}
				$text .= " (<b>$optioncount</b>)";
			}
			elsif($optioncount == 2) {
				$text = "At " . $when[2] . "." . $when[1] . ", <b>" . $prog->{name} . "</b> (<b>$optioncount</b>)";
			}
			push @message, $text;
			$optionslist->{$optioncount} = $prog->{id};
			++$optioncount;
		}
		unshift @message, $hint;
		push @message, "<i>Ask again, choose a programme, or see </i>next<i> and </i>previous<i>.</i>";
		$heap->{cursor}->[0]->{options} = $optionslist;
	}
	
	my $message = join("<br>", @message);
	
	$kernel->post("tele", "send", $heap->{buddy_name}, $message);
			
}

sub output_details {
	my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];

	my $hint = $_[ARG0];
	
	my $programme_id = $heap->{cursor}->[1]->{options}->{ $heap->{cursor}->[0]->{'option_chosen'} };

	my $row = &Autobot::FingertipTV::utility::programme_details(
		$heap->{dbh},
		$programme_id );
	
	my $name = $row->{name};
	my $details = $row->{level3} || $row->{level2} || $row->{level1} || $row->{level0} || "";

	my $sth_data= $heap->{dbh}->prepare("SELECT progkey, progvalue FROM programme_data WHERE programme_id = ?")
		or die "Can't prepare query: " . $heap->{dbh}->errstr;
	my $rc_data = $sth_data->execute($programme_id)
		or die "Can't execute query: " . $heap->{dbh}->errstr;

	my %progdata = ();
	while(my $row = $sth_data->fetchrow_hashref) {
		if($row->{progvalue}) {
			$progdata{ $row->{progkey} } = $row->{progvalue};
		}
	}

	$sth_data->finish;

	# assemble message
	my $message = "<b>$name</b>";
	if($progdata{episode}) {
		$message .= ": " . $progdata{episode};
	}
	if($progdata{duration}) {
		$message .= " (" . $progdata{duration} . ")";
	}
	$message .= ". ";
	if($details ne "") {
		$message .= "<i>$details</i>";
	}
	if($progdata{cast}) {
		$message .= " (<i>" . $progdata{cast} . "</i>)";
	}
	my @meta = ();
	if($progdata{repeat}) {
		push @meta, "Rpt";
	}
	if($progdata{subtitle}) {
		push @meta, "Subtitles";
	}
	if($progdata{bw}) {
		push @meta, "B/W";
	}
	if(@meta > 0) {
		$message .= " [" . join(", ", @meta) . "]";
	}

	$kernel->post("tele", "send", $heap->{buddy_name}, $message);

}

sub output_error {
	my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];

	my $hint = $_[ARG0];

	if($hint !~ m/^\s*$/) {
		$kernel->post("tele", "send", $heap->{buddy_name}, $hint);
	}
}



package Autobot::FingertipTV::utility;

use Time::Local;

sub nicetime {
    my $epoch_time = shift;
    my @gmtime = gmtime($epoch_time);
    return $gmtime[2] . "." . $gmtime[1];
}

sub now {
	my @gmtime = gmtime();
	my $epoch_time = epochtime($gmtime[2], $gmtime[1]);
	return $epoch_time;
}

sub epochtime {
	my $hours = shift;
	my $minutes = shift;
	my @gmtime = gmtime();
	my $epoch_time = timegm(0, $minutes, $hours, $gmtime[3], $gmtime[4], $gmtime[5]);
	return $epoch_time;
}

sub later {
	my $epoch_time = shift;

	$epoch_time -= $epoch_time % 1800;
	$epoch_time += 1800;
	
	return $epoch_time;	
}

sub earlier {
	my $epoch_time = shift;

	if($epoch_time % 1800 == 0) {
		$epoch_time -= 1800;
	}
	else {
		$epoch_time -= $epoch_time % 1800;
	}
	
	return $epoch_time;

}

# fetches from the database a list of programmes and their very basic
# data, using all the cursor information. returns an array of hashref,
# each with:
# - channels.name AS channel,
# - programmes.programme_id AS id,
# - programmes.name AS name,
# - programmes.epoch_time AS epoch_time
# arguments are:
# - dbh
# - cursor
# - how many programmes to fetch per channel
# - the db channelmap
# the programmes in the result list are not necessarily sorted
sub programme_list {
	my $dbh = shift;
	my $cursor = shift;
	my $programmes_per_channel = shift;
	my $channelmap = shift;

	my @channellist = ( $cursor->{channel} )
		|| keys %{$channelmap};
	
	my $statement_stub = <<end;
SELECT
channels.name AS channel,
programmes.programme_id AS id,
programmes.name AS name,
programmes.epoch_time AS epoch_time
FROM channels, channel_programmes, programmes
WHERE channels.name = ?
AND channels.channel_id = channel_programmes.channel_id
AND channel_programmes.programme_id = programmes.programme_id
end

	my @results = ();
	foreach my $channel (@channellist) {
		my @channel_results = ();
		my $db_channelname = $channelmap->{ $channel };
		
		# grab programmes starting 'previous' to the time cursor
		# first (includes on 'now'), and grab ones after to fill
		# space up to the cursor position if necessary
	
		if($cursor->{programme} <= 0) {
			my $limit = abs($cursor->{programme});
			# get as many programmes as are needed to fill the
			# programmes_per_channel amount, with maximum $limit
			my $pagesize = $programmes_per_channel && ($limit + 1);
			
			my $statement_prev = $statement_stub . <<"end";
AND epoch_time <= ?
ORDER BY programmes.epoch_time DESC
LIMIT $limit, $pagesize
end

			my $sth_prev = $dbh->prepare($statement_prev)
				or die "Can't prepare statement: " . $dbh->errstr;

			my $rc_prev = $sth_prev->execute(
				$db_channelname,
				$cursor->{'time'} )
				or die "Can't execute statement: " . $dbh->errstr;

			if($rc_prev > 0) {
				while(my $row = $sth_prev->fetchrow_hashref) {
					push @channel_results, $row;
				}
			}
			
			$sth_prev->finish;
		}
		
		if(@channel_results < $programmes_per_channel) {
			# @channel_results will be 0 if the programme
			# cursor is >0, and might be smaller than the
			# required amount if we're listing a lot of
			# programmes
			
			my $limitstart = $cursor->{programme} > 0
				? $cursor->{programme} - 1
				: 0;
			my $limitmany = $programmes_per_channel - @channel_results;
			
			my $statement_next = $statement_stub . <<"end";
AND epoch_time > ?
ORDER BY epoch_time ASC
LIMIT $limitstart, $limitmany
end

			my $sth_next = $dbh->prepare($statement_next)
				or die "Can't prepare statement: " . $dbh->errstr;

			my $rc_next = $sth_next->execute(
				$db_channelname,
				$cursor->{'time'} )
				or die "Can't execute statement: " . $dbh->errstr;

			if($rc_next > 0) {
				while(my $row = $sth_next->fetchrow_hashref) {
					push @channel_results, $row;
				}
			}
		}
		
		push @results, (sort {
			$a->{epoch_time} <=> $b->{epoch_time}
		} @channel_results);
	}

	return @results;

}

# given a programme id, will return details about it
sub programme_details {
	my $dbh = shift;
	my $programme_id = shift;

	my $statement = <<end;
SELECT
programmes.name AS name,
programmes.epoch_time AS epoch_time,
programme_details.level0 AS level0,
programme_details.level1 AS level1,
programme_details.level2 AS level2,
programme_details.level3 AS level3
FROM programmes, programme_details
WHERE programmes.programme_id = programme_details.programme_id
AND programmes.programme_id = ?
end

	my $sth = $dbh->prepare($statement)
		or die "Can't prepare statement: " . $dbh->errstr;
	
	my $rc = $sth->execute($programme_id)
		or die "Can't execute statement: " . $dbh->errstr;
	
	my $row = $sth->fetchrow_hashref;
	$sth->finish;

	return $row;
}

1;
