#
# Copyright (C) 2008, by petschge <mail@petschge.de>
#

use strict;
use Irssi 20071006;
use Irssi::TextUI;
use Text::Aspell;

use vars qw{$VERSION %IRSSI};
($VERSION) = '0.01';
%IRSSI = (
   name        => 'aspell',
   authors     => 'petschge',
   contact     => 'mail@petschge.de',
   license     => 'GPL',
   description => 'a spell checking plugin using aspell, with on the fly highlighting and easy selection of suggested corrections',
);


my %spellers = ();  # hash of aspell objects (one per language)
my $pending = 0;   # on startup we don't have pending timers
my $linecount = 0; # on startup we don't have any lines for suggestions printed to the current window
my $showhint = 0;  # on startup we are not currently showing any suggestions
my %chanlang = (); # Server->Channel->Language hash of hashes

#extract word under cursor
sub getword {
	my ($line, $pos) = @_;

	my $start = rindex($line, " ", $pos) + 1;
	my $stop = index($line, " ", $pos);
	if($stop == -1) {
		$stop = length($line);
	}
	if($start >= $stop) {
		return "";
	}
	my $len = $stop - $start;
	my $word = substr($line, $start, $len);
	return $word;
}

# change word under cursor
sub setword {
	my ($line, $pos, $word) = @_;
	my $start = rindex($line, " ", $pos) + 1;
	my $stop = index($line, " ", $pos);
	if($stop == -1) {
		$stop = length($line);
	}
	if($start >= $stop) {
		return $line;
	}
	my $len = $stop - $start;
	substr($line, $start, $len) = $word;
	return $line;
}

# spell check current input line
sub check {
	# get window
	my $win = Irssi::active_win();
	my $view = Irssi::UI::Window::view($win);
	my $name = $win->{name};
	my $lang = getlang();
	my $speller = getspeller($lang);

	# remove current hinting
	my $lineref = Irssi::TextUI::TextBufferView::get_bookmark($view, "line");
	Irssi::TextUI::TextBufferView::remove_line($view, $lineref) if defined $lineref;
	Irssi::TextUI::TextBufferView::set_bookmark($view, "line", undef);

	# get input line
	my $line = Irssi::parse_special('$L');
	my $pos = Irssi::gui_input_get_pos();

	# don't do fancy stuff in the status win
	if($name eq "(status)") {
		$pending = 0;
		return;
	}

	my $charsdone = 0;
	my $correction = 0;
	# split into works, check, mark and merge
	my @words = split(/ /, $line);
	my @checked = ();
	foreach (@words) {
		my $word;
		my $punct;
		if($_ =~ /(.*[^\W])(\W+)$/) {
			$word = $1;
			$punct = $2;
		} else {
			$word = $_;
			$punct = '';
		}
		if ($speller->check($word)) {
			push(@checked, $_);
		} else {
			push(@checked, "%r". $word . "%n" . $punct);
			if($charsdone <= $pos) {
				$correction += 2;
			}
			if($charsdone + length($word) <= $pos) {
				$correction += 2;
			}
		}
		$charsdone += length($_) + 1;
	}
	$line = join(" ", @checked);

	# write to window
	if($line =~ /\S+/ ) {
		$line .= ' ' x ($pos + $correction + 2 - length($line));
		my $char = substr($line, $pos + $correction, 1);
		substr($line, $pos + $correction, 1) = "%8$char%8";

		$win->print($line);
		Irssi::TextUI::TextBufferView::set_bookmark_bottom($view, "line");
	}

	Irssi::TextUI::TextBufferView::redraw($view);

	# we are done
	$pending = 0;
}

# display hints for the word under the cursor
sub hintstart {
	my $win = Irssi::active_win();
	my $view = Irssi::UI::Window::view($win);
	my $lang = getlang();
	my $speller = getspeller($lang);

	#get the word where the cursor is
	my $line = Irssi::parse_special('$L');
	my $pos = Irssi::gui_input_get_pos();
	my $word = getword($line, $pos);
	return if($word eq "");

	$showhint = 1;
	$win->print("suggestions for $word:");
	$linecount += 1;
	Irssi::TextUI::TextBufferView::set_bookmark_bottom($view, "bookmark-$linecount");

	$win->print("(0) add $word to dictionary");
	$linecount += 1;
	Irssi::TextUI::TextBufferView::set_bookmark_bottom($view, "bookmark-$linecount");

	my @suggestions = $speller->suggest($word);
	my $j = 1;
	my $maxhints = Irssi::settings_get_int("maxhints");
	foreach(@suggestions) {
		if($j <= $maxhints) {
			$win->print("($j) ". $_);
			$linecount += 1;
			Irssi::TextUI::TextBufferView::set_bookmark_bottom($view, "bookmark-$linecount");
		}
		$j++;
	}
}

#remove display of hints
sub hintend {
	my $win = Irssi::active_win();
	my $view = Irssi::UI::Window::view($win);

	for(my $i = $linecount; $i > 0; $i--) {
		my $line = Irssi::TextUI::TextBufferView::get_bookmark($view, "bookmark-$i");
		Irssi::TextUI::TextBufferView::remove_line($view, $line) if defined $line;
		Irssi::TextUI::TextBufferView::set_bookmark($view, "bookmark-$linecount", undef);
	}
	Irssi::TextUI::TextBufferView::redraw($view);
	$showhint = 0;
}

# handle "gui key pressed"
sub keydown {
	my $key = shift;
	my $win = Irssi::active_win();
	my $lang = getlang();
	my $speller = getspeller($lang);

	# handle suggestion selection
	$key -= 48; # ascii code for 0
	my $maxhints = Irssi::settings_get_int("maxhints");
	if(($showhint == 1) && ($key >= 0) && ($key < 10) && ($key < $maxhints)) {

		#get the word where the cursor is
		my $line = Irssi::parse_special('$L');
		my $pos = Irssi::gui_input_get_pos();
		my $word = getword($line, $pos);
		if(!($word eq "")) {
			if($key == 0) {
				$win->print("added $word to personal dictionary");
				$speller->add_to_personal($word); # adds to .aspell.$lang.pws
				$speller->save_all_word_lists();
			} else {
				# get the suggestion which was picked
				my @suggestions = $speller->suggest($word);
				#$win->print("picking suggestion #".$key.": ".$suggestions[$key - 1]);
				# replace it in the input line
				$line = setword($line, $pos, $suggestions[$key - 1]);
				Irssi::gui_input_set($line);
				Irssi::gui_input_set_pos($pos);
			}
		}
		# kill the key press
		Irssi::signal_stop();
		# and remove the selection
		hintend();
	}

	# spell check again after key press has been included in input line
	if  ($pending == 0) {
		$pending = 1;
		Irssi::timeout_add_once(100, "check", "");
	}
}

# toggle display of hints
sub hint {
	if ($showhint == 0) {
		hintstart();
	} else {
		hintend();
	}
}

sub parse_chanlang {
	my @parts = split(/ /, shift);
	my ($server, $chan, $lang);
	%chanlang = ();
	while(@parts && (@parts >= 3)) {
		$server = shift(@parts);
		$chan = shift(@parts);
		$lang = shift(@parts);

		my %a;
		if (exists $chanlang{$server}) {
			%a = %{$chanlang{$server}};
		} else {
			%a = ();
		}
		$a{$chan} = $lang;
		$chanlang{$server} = \%a;
	}
}

sub chanlang_to_string {
	my @parts = ();
	foreach my $server (keys %chanlang) {
	my %a = %{$chanlang{$server}};
		foreach my $chan (keys %a) {
			push(@parts, "$server $chan ".$a{$chan});
		}
	}
	my $string = join(" ", @parts);
	return $string;
}

sub showchanlang {
	my $win = Irssi::active_win();
	foreach my $server (keys %chanlang) {
	$win->print ("for server $server:");
		my %a = %{$chanlang{$server}};
		foreach my $chan (keys %a) {
			$win->print ("    $chan: ".$a{$chan});
		}
	}
}

sub delete_from_chanlang {
	my $server = shift;
	my $chan = shift;

	my %a = %{$chanlang{$server}};
	delete($chanlang{$server});
	delete($a{$chan});
	$chanlang{$server} = \%a if keys %a;
	Irssi::settings_set_str("chanlang", chanlang_to_string());
}

sub add_to_chanlang {
	my $server = shift;
	my $chan = shift;
	my $lang = shift;

	my %a;
	if (exists $chanlang{$server}) {
		%a = %{$chanlang{$server}};
	} else {
		%a = ();
	}
	$a{$chan} = $lang;
	$chanlang{$server} = \%a;
	Irssi::settings_set_str("chanlang", chanlang_to_string());
}

sub getlang {
	my $server = Irssi::active_server();
	my $tag = $server->{tag};
	my $win = Irssi::active_win();
	my $active = $win->{active};
	my $chan = $active->{name};

	my $lang = Irssi::settings_get_str("defaultlang");
	if (exists $chanlang{$tag}) {
		if (exists $chanlang{$tag}{$chan}) {
			$lang = $chanlang{$tag}{$chan};
		}
	}

	return $lang;
}

sub getspeller {
	my $lang = shift;

	if(exists $spellers{$lang}) {
		return $spellers{$lang};
	} else {
		my $speller = Text::Aspell->new;
		die unless $speller;
		$speller->set_option('lang', $lang);
		$speller->set_option('sug-mode', Irssi::settings_get_str("sugmode"));
		$speller->set_option('keyboard', Irssi::settings_get_str("keyboard"));
		$speller->set_option('ignore-case', Irssi::settings_get_str("ignorecase"));
		#implicit $speller->create_speller;
		$spellers{$lang} = $speller;
		return $speller;
	}
}

sub showsettings {
	my $win = Irssi::active_win();
	$win->print("aspell settings:");
	$win->print("maxhints is currently ".Irssi::settings_get_int("maxhints"));
	$win->print("defaultlang is currently ".Irssi::settings_get_str("defaultlang"));
	$win->print("sugmode is currently ".Irssi::settings_get_str("sugmode"));
	$win->print("keyboard is currently ".Irssi::settings_get_str("keyboard"));
	$win->print("ignorecase is currently ".Irssi::settings_get_str("ignorecase"));
	$win->print("channel exceptions are:");
	showchanlang();
}

sub maxhints {
	my $maxhints = shift;
	my $win = Irssi::active_win();
	if($maxhints eq "") {
		$win->print("aspell maxhints is currently ".Irssi::settings_get_int("maxhints"));
	} elsif ($maxhints =~ /^ (\d+)/) {
		Irssi::settings_set_int("maxhints", $1);
		$win->print("aspell maxhints set to ".Irssi::settings_get_int("maxhints"));
	}
}

sub defaultlang {
	my $defaultlang = shift;
	my $win = Irssi::active_win();
	if($defaultlang eq "") {
		$win->print("aspell default language is currently ".Irssi::settings_get_str("defaultlang"));
	} elsif ($defaultlang =~ /^ (\S+)/ ) {
		Irssi::settings_set_str("defaultlang", $1);
		$win->print("aspell defaultlang set to ".Irssi::settings_get_str("defaultlang"));
	}
}

sub sugmode {
	my $sugmode = shift;
	my $win = Irssi::active_win();
	if($sugmode eq "") {
		$win->print("aspell suggestion mode is currently ".Irssi::settings_get_str("sugmode"));
	} elsif ($sugmode =~ /^ (ultra|fast|normal|bad-spellers)/) {
		Irssi::settings_set_str("sugmode", $1);
		$win->print("aspell suggestion mode set to ".Irssi::settings_get_str("sugmode"));
	}
}

sub keyboard {
	my $keyboard = shift;
	my $win = Irssi::active_win();
	if($keyboard eq "") {
		$win->print("aspell keyboard is currently ".Irssi::settings_get_str("keyboard"));
	} elsif ($keyboard =~ /^ (dvorak|split|standard)/ ) {
		Irssi::settings_set_str("keyboard", $1);
		$win->print("aspell keyboard set to ".Irssi::settings_get_str("keyboard"));
	}
}

sub ignorecase {
	my $ignorecase = shift;
	my $win = Irssi::active_win();
	if($ignorecase eq "") {
		$win->print("aspell ignorecase is currently ".Irssi::settings_get_str("ignorecase"));
	} elsif ($ignorecase =~ /^ (true|false)/ ) {
		Irssi::settings_set_str("ignorecase", $1);
		$win->print("aspell ignorecase set to ".Irssi::settings_get_str("ignorecase"));
	}
}

sub showstats {
	my $win = Irssi::active_win();
	$win->print("Currently ".keys(%spellers)." aspell engines");
}

# run on /aspell
sub aspell {
	my $win = Irssi::active_win();
	my $cmd = shift;
	if($cmd eq "") {
		$win->print("please use \"aspell hint\" to get a hint for the word underneath the cursor, or \"aspell set\" to edit the aspell settings. The language for a single channel can be set with \"aspell chanlang (add|remove)\".");
	} elsif($cmd =~ /^hint/) {
		hint();
	} elsif($cmd =~ /^set(.*)/) {
		if($1 eq "") {
			showsettings();
		} elsif($1 =~ /^ maxhints(.*)/) {
			maxhints($1);
		} elsif($1 =~ /^ defaultlang(.*)/) {
			defaultlang($1);
		} elsif($1 =~ /^ sugmode(.*)/) {
			sugmode($1);
			%spellers = ();
		} elsif($1 =~ /^ keyboard(.*)/) {
			keyboard($1);
			%spellers = ();
		} elsif($1 =~ /^ ignorecase(.*)/) {
			ignorecase($1);
			%spellers = ();
		} else {
			$win->print("aspell unknown setting: $1");
		}
	} elsif($cmd =~ /^chanlang(.*)/) {
		if($1 eq "") {
			showchanlang();
		} elsif($1 =~ /^ add ([^ ]+) ([^ ]+) ([^ ]+)/) { # server chan lang
			add_to_chanlang($1, $2, $3);
		} elsif($1 =~ /^ remove ([^ ]+) ([^ ]+)/) { # server chan lang
			delete_from_chanlang($1, $2);
		}
		parse_chanlang(Irssi::settings_get_str("chanlang"));
		$win->print("channel exceptions are:");
		showchanlang();
	} elsif($cmd =~ /^stat(.*)/) {
		showstats();
	} else {
		$win->print("unknown aspell command: $cmd");
	}
}

# run on start up
Irssi::settings_add_int("aspell", "maxhints", "5");
Irssi::settings_add_str("aspell", "defaultlang", "en_US");
Irssi::settings_add_str("aspell", "sugmode", "slow");
Irssi::settings_add_str("aspell", "keyboard", "standard");
Irssi::settings_add_str("aspell", "ignorecase", "false");
Irssi::settings_add_str("aspell", "chanlang", "");
parse_chanlang(Irssi::settings_get_str("chanlang"));

# bind functions
Irssi::command_bind("aspell", "aspell");
Irssi::command("/bind ^S /aspell hint");
Irssi::signal_add("gui key pressed", "keydown");
