# INSTALLATION
# [&bitlbee] set typing_notice true
# <@root> typing_notice = `true'
# AND
# /statusbar window add typing_notice
#
# SETTINGS
# [bitlbee]
# bitlbee_send_typing = ON
#   -> send typing messages to buddies
# bitlbee_typing_allwin = OFF
#   -> show typing notifications in all windows
#
# 
# Changelog:
#
# 2016-01-01 (version 1.7.2)
# * Fix crash in Irssi during disconnect
#
# 2010-08-09 (version 1.7.1)
# * Multiple control channels supported by checking chanmodes
#
# 2010-07-27 (version 1.7)
# * Using new server detection for latest BitlBee support
#
# 2010-07-26 (version 1.6.3)
# * Removed checking if nicks exists in &bitlbee channel, this because BitlBee 
#   can be used without control channel from this date
#
# 2007-03-03 (version 1.6.2)
# * Fix: timers weren't deleted correctly. This resulted in huge mem usage.
#
# 2006-11-02 (version 1.6.1)
# * Sending typing works again.
#
# 2006-10-27 (version 1.6)
# * 'channel sync' re-implemented.
# * bitlbee_send_typing was a string setting, It's a boolean now, like it should.
#
# 2006-10-24 (version 1.5)
# * Sending notices to online users only. ( removed this again at 2010-07-26, see above )
# * Using the new get_channel function;
#
# 2005-12-15 (version 1.42):
# * Fixed small bug with typing notices disappearing under certain circumstances
#   in channels
# * Fixed bug that caused outgoing notifications not to work 
# * root cares not about our typing status.
#
# 2005-12-04 (version 1.41):
# * Implemented stale states in statusbar (shows "(stale)" for OSCAR connections) 
# * Introduced bitlbee_typing_allwin (default OFF). Set this to ON to make
#   typing notifications visible in all windows.
#
# 2005-12-03 (version 1.4):
# * Major code cleanups and rewrites for bitlbee 1.0 with the updated typing
#   scheme. TYPING 0, TYPING 1, and TYPING 2 are now supported from the server.
# * Stale states (where user has typed in text but has stopped typing) are now
#   recognized.
# * Bug where user thinks you are still typing if you close the window after
#   typing something and then erasing it quickly.. fixed.
# * If a user signs off while they are still typing, the notification is removed
# This update by Matt "f0rked" Sparks
#
# 2005-08-26:
# Some fixes for AIM, Thanks to Dracula.
#
# 2005-08-16:
# AIM supported, for sending notices, using CTCP TYPING 0. (Use the AIM patch from Hanji http://get.bitlbee.org/patches/)
# 
# 2004-10-31:
# Sends typing notice to the bitlbee server when typing a message in irssi. bitlbee > 0.92
#
# 2004-06-11:
# shows [typing: ] in &bitlbee with multiple users.
#
use strict;
use Irssi::TextUI;
use Data::Dumper;

use vars qw($VERSION %IRSSI);

$VERSION = '1.7.2';
%IRSSI = (
	authors	 	=> 'Tijmen "timing" Ruizendaal, Matt "f0rked" Sparks',
	contact		=> 'tijmen.ruizendaal@gmail.com, root@f0rked.com',
	name		=> 'BitlBee_typing_notice',
	description	=> '1. Adds an item to the status bar wich shows [typing] when someone is typing a message on the supported IM-networks	2. Sends typing notices to the supported IM networks (the other way arround). (For bitlbee 3.0+)',
	sbitems 	=> 'typing_notice',
	license	 	=> 'GPLv2',
	url		=> 'http://the-timing.nl/stuff/irssi-bitlbee, http://f0rked.com',
);

my %bitlbee_tag; # server object
my %control_channels; # mostly: &bitlbee, &facebook etc.
init();

sub init { # if script is loaded after connect
	my @servers = Irssi::servers();
	foreach my $server(@servers) {
		if( $server->isupport('NETWORK') eq 'BitlBee' ){
			my $T = $server->{tag};
			$bitlbee_tag{$T} = 1;
			my @channels = $server->channels();
			foreach my $channel(@channels) {
				if( $channel->{mode} =~ /C/ ){
					push @{ $control_channels{$T} }, $channel->{name} unless (grep $_ eq $channel->{name}, @{ $control_channels{$T} // [] });
				}
			}
		}
	}
}
# if connect after script is loaded
Irssi::signal_add_last('event 005' => sub {
	my( $server ) = @_;
	if( $server->isupport('NETWORK') eq 'BitlBee' ){
		$bitlbee_tag{ $server->{tag} } = 1;
	}
});
# if new control channel is synced after script is loaded
Irssi::signal_add_last('channel sync' => sub {
	my( $channel ) = @_;
	my $T = $channel->{server}->{tag};
	if( $channel->{mode} =~ /C/ && $bitlbee_tag{$T} ){
		push @{ $control_channels{$T} }, $channel->{name} unless (grep $_ eq $channel->{name}, @{ $control_channels{$T} // [] });
	}
});

# How often to check if we are typing, or on msn,
# how long to keep the typing notice up, or check
# if the other user is still typing...
my $KEEP_TYPING_TIMEOUT = 1;
my $STOP_TYPING_TIMEOUT = 7;

my %timer_tag;

my %typing;
my %tag;
my $line;
my %out_typing;
my $lastkey;
my $keylog_active = 1;
my $command_char = Irssi::settings_get_str('cmdchars'); # mostly: /
my $to_char = Irssi::settings_get_str("completion_char"); # mostly: :

sub event_ctcp_msg {
	my ($server, $msg, $from, $address) = @_;
	my $tag = $server->{tag};
	return unless $bitlbee_tag{ $tag };
	if ( my($type) = $msg =~ "TYPING ([0-9])" ){
		Irssi::signal_stop();
		if( $type == 0 ){
			unset_typing($tag, $from);
		} elsif( $type == 1 ){
			my $k = "$tag/$from";
			$typing{$k}=1;
			if( $address !~ /\@login\.oscar\.aol\.com/ and $address !~ /\@YAHOO/ and $address !~ /\@login\.icq\.com/ ){
				Irssi::timeout_remove($tag{$k});
				delete($tag{$k});
				$tag{$k}=Irssi::timeout_add_once($STOP_TYPING_TIMEOUT*1000,"unset_typing",[$tag,$from]);
			}
			redraw($tag, $from);
		} elsif( $type == 2 ){
			stale_typing($tag, $from);
		}
	}
}

sub unset_typing {
	my($tag,$from,$no_redraw)=@_;
	my $k = "$tag/$from";
	delete $typing{$k} if $typing{$k};
	Irssi::timeout_remove($tag{$k});
	delete($tag{$k});
	redraw($tag, $from) if !$no_redraw;
}

sub stale_typing {
	my($tag,$from)=@_;
	my $k = "$tag/$from";
	$typing{$k}=2;
	redraw($tag, $from);
}

sub redraw {
	my($tag,$from)=@_;
	my $window = Irssi::active_win();
	my $name = $window->get_active_name();
	
	# only redraw if current window equals to the typing person, is a control channel or if allwin is set
	if( $from eq $name || (grep $_ eq $name, @{ $control_channels{$tag} // [] }) || Irssi::settings_get_bool("bitlbee_typing_allwin") ){
		Irssi::statusbar_items_redraw('typing_notice');
	}
}	

sub event_msg {
	my ($server,$data,$from,$address,$target) = @_;
	my $tag = $server->{tag};
	return unless $bitlbee_tag{ $tag };
	my $channel=Irssi::active_win()->get_active_name();
	unset_typing $tag, $from, "no redraw";
	unset_typing $tag, $channel;
}

sub event_quit {
	my $server = shift;
	my $tag = $server->{tag};
	return unless $bitlbee_tag{ $tag };
	my $nick = shift;
	unset_typing $tag, $nick;
}

sub typing_notice {
	my ($item, $get_size_only) = @_;
	my $window = Irssi::active_win();
	my $tag = Irssi::active_server->{tag};
	my $channel = $window->get_active_name();
	my $k = "$tag/$channel";
	
	if (exists($typing{$k})) {
		my $append=$typing{$k}==2 ? " (stale)" : "";
		$item->default_handler($get_size_only, "{sb typing$append}", 0, 1);
	} else {
		$item->default_handler($get_size_only, "", 0, 1);
		Irssi::timeout_remove($tag{$k});
		delete($tag{$k});
	}
	# we check for correct windows again, because the statusbar item is redrawn after window change too.
	if( (grep $_ eq $channel, @{ $control_channels{$tag} // [] }) || Irssi::settings_get_bool("bitlbee_typing_allwin")) {
		foreach my $key (sort keys(%typing)) {
			$line .= " ".$key;
			if ($typing{$key}==2) { $line .= " (stale)"; }
		}
		if ($line ne "") {
			$item->default_handler($get_size_only, "{sb typing:$line}", 0, 1);
			$line = "";
		}
	} 
}

sub window_change {
	Irssi::statusbar_items_redraw('typing_notice');
	my $win = !Irssi::active_win() ? undef : Irssi::active_win()->{active};
	if (ref $win && defined $win->{server}->{tag} && $bitlbee_tag{ $win->{server}->{tag} }) {
		if (!$keylog_active) {
			$keylog_active = 1;
			Irssi::signal_add_last('gui key pressed', 'key_pressed');
		}
	} else {
		if ($keylog_active) {
			$keylog_active = 0;
			Irssi::signal_remove('gui key pressed', 'key_pressed');
		}
	}
}

sub key_pressed {
	return if !Irssi::settings_get_bool("bitlbee_send_typing");
	my $key = shift;
	if ($key != 9 && $key != 10 && $key != 13 && $lastkey != 27 && $key != 27 
	   && $lastkey != 91 && $key != 126 && $key != 127) 
	{
		my $server = Irssi::active_server();
		my $window = Irssi::active_win();
		my $nick = $window->get_active_name();
		my $tag = $server->{tag};
		if (defined $tag && $bitlbee_tag{ $tag } && $nick ne "(status)" && $nick ne "root") {
			if( grep $_ eq $nick, @{ $control_channels{$tag} // [] } ){ # send typing if in control channel
				my $input = Irssi::parse_special("\$L");
				my ($first_word) = split(/ /,$input);
				if ($input !~ /^$command_char.*/ && $first_word =~ s/$to_char$//){
					send_typing($tag, $first_word);
				}
			} else { # or any other channels / query
				my $input = Irssi::parse_special("\$L");
				if ($input !~ /^$command_char.*/ && length($input) > 0){
					send_typing($tag, $nick);
				}
			}
		}
	}
	$lastkey = $key;
}

sub delete_server {
	my $tag = shift;
	delete $bitlbee_tag{$tag};
	delete $control_channels{$tag};
	undef;
}

sub out_empty {
	my ($a) = @_;
	my($nick,$tag)=@{$a};
	my $k = "$tag/$nick";
	delete($out_typing{$k});
	Irssi::timeout_remove($timer_tag{$k});
	delete($timer_tag{$k});
	if (my $bitlbee_server = Irssi::server_find_tag($tag)) {
		$bitlbee_server->command("^CTCP $nick TYPING 0");
	} else {
		delete_server($tag);
	}
}

sub send_typing {
	my ($tag, $nick) = @_;
	my $k = "$tag/$nick";
	if (!exists($out_typing{$k}) || time - $out_typing{$k} > $KEEP_TYPING_TIMEOUT) {
		if (my $bitlbee_server = Irssi::server_find_tag($tag)) {
			$bitlbee_server->command("^CTCP $nick TYPING 1");
		} else {
			delete_server($tag);
		}
		$out_typing{$k} = time;
		### Reset 'stop-typing' timer
		Irssi::timeout_remove($timer_tag{$k});
		delete($timer_tag{$k});

		### create new timer
		$timer_tag{$k} = Irssi::timeout_add_once($STOP_TYPING_TIMEOUT*1000, 'out_empty', ["$nick", $tag]);
	}
}

#README: Delete the old bitlbee_send_typing string from ~/.irssi/config. A boolean is better.

sub db_typing {
	local $Data::Dumper::Sortkeys = 1;
	print "Detected channels: ";
	print Dumper(%control_channels);
	print "Detected server tag: ".join ", ", sort keys %bitlbee_tag;
	print "Tag: ".Dumper(%tag);	
	print "Timer Tag: ".Dumper(%timer_tag);	
	print "Typing: ".Dumper(%typing);	
	print "Out Typing: ".Dumper(%out_typing);	
}

Irssi::command_bind('db_typing','db_typing');

Irssi::settings_add_bool("bitlbee","bitlbee_send_typing",1);
Irssi::settings_add_bool("bitlbee","bitlbee_typing_allwin",0);

Irssi::signal_add("ctcp msg", "event_ctcp_msg");
Irssi::signal_add("message private", "event_msg");
Irssi::signal_add("message public", "event_msg");
Irssi::signal_add("message quit", "event_quit");
Irssi::signal_add_last('window changed', 'window_change');
Irssi::signal_add_last('gui key pressed', 'key_pressed');
Irssi::statusbar_item_register('typing_notice', undef, 'typing_notice');
Irssi::statusbars_recreate_items();
