################################################
## irssi quiz (iQuiz) script by wilk/xorandor ##
################################################

# Quiz accepts:
# - standard Dizzy files (also without "pyt"/"odp" prefixes):
#
# pyt Question_1
# odp Answer_1
# pyt Question_2
# odp Answer_2
# ...
#
# - standard Mieszacz files (also without line numbers):
#
# 1 alpha
# 2 beta
# 3 gamma
# 4 delta
# ...

# >>> To check quiz commands and user settings type: /quiz

use strict;
use warnings;
use vars qw($VERSION %IRSSI);
use Irssi;

$VERSION = '130627';
%IRSSI = (
	authors		=> 'wilk',
	name		=> 'iQuiz',
	description	=> 'irssi quiz script',
	license		=> 'GNU GPL v3 or any later version',
	changed		=> $VERSION,
	url			=> 'http://www.quizpl.net/pliki/quiz.pl'
);

##### Internal stuff #####
my %quiz = (chanrec => undef,
			ison => 0, inq => 0,
			stime => 0, qtime => 0,
			qnum => 0, hnum => 0,
			tnext => undef, thint => undef, tremind => undef,
			hprot => 0, rprot => 0);
my (@quiz, %score, %nicks, %ts, @dots, @hwords, @words, $question, $answer);

##### Theme (only channel messages are localized by default, feel free to customize, except authorship) #####
# quiz_err_* messages are irssi only - use irssi color codes
# quiz_msg_* messages are sent on channel - use mIRC color codes:
# \002 - bold  \003$fg(,$bg)? - color  \017 - plain  \026 - reverse  \037 - underline
Irssi::theme_register([
	'quiz_err_ison',		'%_iQuiz:%_ %RQuiz is already on%n',
	'quiz_err_isoff',		'%_iQuiz:%_ %RQuiz is already off%n',
	'quiz_err_server',		'%_iQuiz:%_ %RNot connected to server%n',
	'quiz_err_channel',		'%_iQuiz:%_ %RInvalid channel%n',
	'quiz_err_nochannel',	'%_iQuiz:%_ %RChannel $0 is not open%n',
	'quiz_err_filename',	'%_iQuiz:%_ %RInvalid filename%n',
	'quiz_err_nofile',		'%_iQuiz:%_ %RFile \'$0\' not found%n',
	'quiz_err_file',		'%_iQuiz:%_ %RFile \'$0\' seems to be broken%n',
	'quiz_err_argument',	'%_iQuiz:%_ %RInvalid argument%n',
	'quiz_err_noquestion',	'%_iQuiz:%_ %RNo question asked%n',
	'quiz_err_type',		'%_iQuiz:%_ %RInvalid quiz type%n',

	'quiz_msg',					"%s", # le trick (custom text)
	'quiz_msg_start1',			"\00303>>> \00310iQuiz by wilk wystartowal \00303<<<",
	'quiz_msg_start2',			"\00303Polecenia: !podp, !przyp, !ile, !ile nick",
	'quiz_msg_stop1',			"\00303>>> \00310iQuiz zakonczony \00303<<<",
	'quiz_msg_stop2',			"\00303Ilosc rund: \00304%u \00303Czas gry: \00304%s",
	'quiz_msg_question',		"\00303\037Pytanie %u/%u:\037 \00304%s",
	'quiz_msg_question_2',		"\00303\037Haslo %u/%u:\037 \00304%s",
	'quiz_msg_hint',			"\00303Podpowiedz: \00304%s",
	'quiz_msg_dot',				"\00310.\00304",
	'quiz_msg_remind',			"\00303Przypomnienie pytania: \00304%s",
	'quiz_msg_remind_2',		"\00303Przypomnienie hasla: \00304%s",
	'quiz_msg_delay',			"\00303Opoznienie miedzy pytaniami: \00304%s",
	'quiz_msg_score',			"\00304%s\00303\002\002, zdobyles(as) jak dotad \00304%u\00303 %s!",
	'quiz_msg_noscore',			"\00304%s\00303\002\002, nie zdobyles(as) jeszcze zadnego punktu!",
	'quiz_msg_scorex',			"\00304%s\00303 zdobyl(a) jak dotad \00304%u\00303 %s!",
	'quiz_msg_noscorex',		"\00304%s\00303 nie zdobyl(a) jeszcze zadnego punktu!",
	'quiz_msg_noscores',		"\00303Tablica wynikow jest jeszcze pusta.",
	'quiz_msg_scores',			"\00303Wyniki quizu po \00304%s\00303:",
	'quiz_msg_scores_rank',		"\00303%u. miejsce: \00304%s\00303 - \00304%u\00303 %s",
	'quiz_msg_congrats',		"\00303Brawo, \00304%s\00303! Dostajesz %s za odpowiedz \00304%s\00303 podana po czasie %s. Suma punktow: \00304%u\00303.",
	'quiz_msg_got_points',		"\00304%u\00303 %s",
	'quiz_msg_got_point',		"\00303%s",
	'quiz_msg_next',			"\00303Nastepne pytanie za %s...",
	'quiz_msg_next_2',			"\00303Nastepne haslo za %s...",
	'quiz_msg_last',			"\00307Koniec pytan!",
	'quiz_msg_skipped',			"\00303Pytanie zostalo pominiete.",
	'quiz_msg_point',			"punkt",	# 1 point 			/ 1 punkt
	'quiz_msg_points_234',		"punkty",	# 2-4, ?2-?4 points	/ 2-4, ?2-?4 punkty
	'quiz_msg_points_234_1x',	"punktow",	# 11-14 points		/ 11-14 punktow
	'quiz_msg_points',			"punktow",	# x points			/ x punktow
	'quiz_msg_hours',			"%u godz.",
	'quiz_msg_minutes',			"%u min.",
	'quiz_msg_seconds',			"%u sek.",
]);

##### Main routines #####
sub load_quiz {
	undef @quiz;
	local *F;
	my $lines = 0;
	my $type = Irssi::settings_get_int('quiz_type');
	if (open(F, shift)) {
		while (<F>) {
			s/[\n\r]//g;
			if ($type == 1) {
				if ($lines % 2) { s/^odp //i; } else { s/^pyt //i; };
			} elsif ($type == 2) {
				s/^\d+ //;
			}
			next if (/^$/);
			push(@quiz, $_);
			$lines++;
		}
		close F;
	}
	return $lines;
}

sub get_format {
	my ($tag, $args) = @_;
	$tag = $1 if ($tag =~ /^!(.+)/);
	my @args = (ref $args) ? @{$args} : ();
	return ($tag eq '') ? $args : sprintf(Irssi::current_theme()->get_format('Irssi::Script::quiz', $tag), @args);
}

sub send_msg {
	my ($tag) = @_;
	my $msg = get_format(@_);
	if ($quiz{chanrec}{server}{connected}) {
		if ($tag =~ /^!/) { # '!' prefix for instant message otherwise queued
			$quiz{chanrec}{server}->send_raw_now('PRIVMSG '.$quiz{chanrec}{name}.' :'.$msg);
		} else {
			$quiz{chanrec}{server}->send_raw('PRIVMSG '.$quiz{chanrec}{name}.' :'.$msg);
		}
		Irssi::timeout_add_once(100, 'evt_delayed_show_msg', $msg); # le trick (fix for chantext after owntext)
	} else {
		print CLIENTCRAP $msg;
		Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_server');
	}
}

sub shuffle {
	my ($old, $new, $trap) = (shift, '', 10); # $trap is a watchdog
	do {
		my @letters = split(//, $old);
		$new = '';
		while (@letters) {
			$new .= splice(@letters, int(rand($#letters + 1)), 1);
		}
	} until (($old ne $new) || ($trap-- == 0));
	return $new;
}

sub confuse {
	(my $text = shift) =~ s/ //g;
	return shuffle($text);
}

sub correct_answer {
	my ($nick, $addr) = @_;
	my $ts = time();
	my $points = Irssi::settings_get_int('quiz_points_per_answer');
	($nicks{$addr}, $ts{$addr}) = ($nick, $ts);
	$score{$addr} += $points;
	my $msg = get_format('quiz_msg_congrats', [$nick, ($points > 1) ? get_format('quiz_msg_got_points', [$points, ord_score($points)]) : get_format('quiz_msg_got_point', [ord_score($points)]), $answer, time_str($ts - $quiz{qtime}), $score{$addr}]);
	my $type = Irssi::settings_get_int('quiz_type');
	if ((($type == 1) && ($quiz{qnum} * 2 > $#quiz)) || (($type == 2) && ($quiz{qnum} > $#quiz))) {
		send_msg('!quiz_msg', ["$msg ".get_format('quiz_msg_last')]);
	} else {
		my $delay = Irssi::settings_get_int('quiz_delay');
		send_msg('!quiz_msg', ["$msg ".get_format(($type == 2) ? 'quiz_msg_next_2' : 'quiz_msg_next', [time_str($delay)])]);
		$quiz{tnext} = Irssi::timeout_add_once($delay * 1000, 'evt_next_question', undef);
	}
}

sub make_hint {
	if ($#dots == -1) {
		my $w = 0;
		foreach my $word (@words) {
			my @letters = split(//, $word);
			my $l = 0;
			my $hword = '';
			foreach my $letter (@letters) {
				if ($letter =~ /[a-z0-9]/i) {
					push(@{$dots[$w]}, $l);
					$hword .= "\002"; # le trick (any non-ASCII char)
				} else {
					$hword .= $letter;
				}
				$l++;
			}
			push(@hwords, $hword);
			$w++;
		}
	}
	$quiz{hnum}++;
	my $first_dots = Irssi::settings_get_bool('quiz_first_hint_dots');
	if (!$first_dots || ($first_dots && $quiz{hnum} > 1)) {
		my $w = 0;
		foreach my $rwdots (@dots) {
			if (ref $rwdots) {
				if (@{$rwdots} > 0) {
					my @letters = split(//, $words[$w]);
					my @hletters = split(//, $hwords[$w]);
					my $sel = Irssi::settings_get_bool('quiz_random_hints') ? int(rand(@{$rwdots})) : 0;
					$hletters[@{$rwdots}[$sel]] = $letters[@{$rwdots}[$sel]];
					$hwords[$w] = join('', @hletters);
					splice(@{$rwdots}, $sel, 1);
				}
			}
			$w++;
		}
	}
	my $hint = join(' ', @hwords);
	my $dot = get_format('quiz_msg_dot');
	$hint =~ s/\002/$dot/g; # le trick grande finale
	if (my ($scol, $ecol) = ($dot =~ /^(\003\d+).(\003\d+)$/)) {
		$hint =~ s/$ecol $scol/ /g; # reduce bloat
		$hint =~ s/$ecol$scol//g; # reduce bloat
	}
	return $hint;
}

sub time_str {
	my $time = shift;
	my $h = $time ? int($time / 3600) : 0;
	my $m = $time ? int($time / 60) - $h * 3600 : 0;
	my $s = $time - $m * 60;
	return ($h ? get_format('quiz_msg_hours', [$h]).' ' : '').($m ? get_format('quiz_msg_minutes', [$m]). ' ' : '').get_format('quiz_msg_seconds', [$s]);
}

sub ord_score {
	my $value = abs(shift);
	if ($value != 1) {
		my @value = split(//, "$value");
		if (($value[$#value] > 1) && ($value[$#value] < 5)) {
			return ($value[$#value - 1] == 1 ? get_format('quiz_msg_points_234_1x') : get_format('quiz_msg_points_234'));
		} else {
			return get_format('quiz_msg_points');
		}
	}
	return get_format('quiz_msg_point');
}

sub stop_quiz {
	my $fullstop = shift;
	($quiz{inq}, $quiz{hprot}, $quiz{rprot}) = (0, 0, 0);
	Irssi::timeout_remove($quiz{thint}), $quiz{thint} = undef if ($quiz{thint});
	Irssi::timeout_remove($quiz{tremind}), $quiz{tremind} = undef if ($quiz{tremind});
	if (defined $fullstop) {
		$quiz{ison} = 0;
		Irssi::timeout_remove($quiz{tnext}), $quiz{tnext} = undef if ($quiz{tnext});
		Irssi::signal_remove('message public', 'sig_pubmsg');
	}
}

##### Commands' handlers #####
sub cmd_start {
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_ison'), return if ($quiz{ison});
	my ($args, $server, $window) = @_;
	my ($chan, $file) = split(/ /, $args);
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_server'), return if (!$server || !$server->{connected});
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_channel'), return if (!$chan || !$server->ischannel($chan));
	$quiz{chanrec} = $server->channel_find($chan);
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_nochannel', $chan), return if (!$quiz{chanrec});
	$file =~ s/^~\/// if ($file =~ /^~\//); # open does not support "~/"?
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_filename'), return if (!$file);
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_nofile', $file), return if (!-e $file);
	my $type = Irssi::settings_get_int('quiz_type');
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_type'), return if (($type < 1) || ($type > 2));
	load_quiz($file);
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_file', $file), return if (($#quiz < 1) || (($type == 1) && (($#quiz + 1) % 2)));
	undef %nicks;
	undef %score;
	undef %ts;
	send_msg('!quiz_msg_start1');
	send_msg('!quiz_msg_start2');
	($quiz{stime}, $quiz{qnum}, $quiz{ison}) = (time(), 0, 1);
	Irssi::signal_add_last('message public', 'sig_pubmsg');
	$quiz{tnext} = Irssi::timeout_add_once(Irssi::settings_get_int('quiz_delay') * 1000, 'evt_next_question', undef);
}

sub cmd_stat {
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_isoff'), return if (!$quiz{ison});
	my ($num) = @_;
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_argument'), return if ($num && ($num !~ /\d+/));
	$num = 0 if (!$num);
	if (scalar keys %score == 0) {
		send_msg('quiz_msg_noscores');
	} else {
		send_msg('quiz_msg_scores', [time_str(time() - $quiz{stime})]);
		my $rank = 1;
		foreach my $it (sort {
							return $score{$b} <=> $score{$a} if ($score{$a} != $score{$b});
							return $ts{$a} <=> $ts{$b};
						} keys %score) {
			send_msg('quiz_msg_scores_rank', [$rank, $nicks{$it}, $score{$it}, ord_score($score{$it})]);
			last if ($rank == $num);
			$rank++;
		}
	}
}

sub cmd_delay {
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_isoff'), return if (!$quiz{ison});
	my ($delay) = @_;
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_argument'), return if (!defined($delay) || ($delay !~ /^\d+$/) || $delay < 1);
	Irssi::settings_set_int('quiz_delay', $delay);
	send_msg('quiz_msg_delay', [time_str($delay)]);
}

sub cmd_skip {
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_isoff'), return if (!$quiz{ison});
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_noquestion'), return if (!$quiz{inq});
	stop_quiz();
	my $msg = get_format('quiz_msg_skipped');
	my $type = Irssi::settings_get_int('quiz_type');
	if ((($type == 1) && ($quiz{qnum} * 2 > $#quiz)) || (($type == 2) && ($quiz{qnum} > $#quiz))) {
		send_msg('quiz_msg', ["$msg ".get_format('quiz_msg_last')]);
	} else {
		my $delay = Irssi::settings_get_int('quiz_delay');
		send_msg('quiz_msg', ["$msg ".get_format(($type == 2) ? 'quiz_msg_next_2' : 'quiz_msg_next', [time_str($delay)])]);
		$quiz{tnext} = Irssi::timeout_add_once($delay * 1000, 'evt_next_question', undef);
	}
}

sub cmd_hint {
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_isoff'), return if (!$quiz{ison});
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_noquestion'), return if (!$quiz{inq});
	send_msg('quiz_msg_hint', [make_hint()]);
}

sub cmd_stop {
	Irssi::printformat(MSGLEVEL_CRAP, 'quiz_err_isoff'), return if (!$quiz{ison});
	stop_quiz(1);
	send_msg('quiz_msg_stop1');
	send_msg('quiz_msg_stop2', [$quiz{qnum}, time_str(time() - $quiz{stime})]);
}

sub cmd_help {
	print CLIENTCRAP $IRSSI{name}." v$VERSION by wilk (quiz is currently: ".($quiz{ison} ? 'on' : 'off').')';
	print CLIENTCRAP 'Available commands:';
	print CLIENTCRAP '   /qon <channel> <path>   - start quiz from file';
	print CLIENTCRAP '   /qstat [num]            - show top x players or all of them';
	print CLIENTCRAP '   /qdelay <sec>           - change delay between questions';
	print CLIENTCRAP '   /qhint                  - enforce next hint';
	print CLIENTCRAP '   /qskip                  - skip current question';
	print CLIENTCRAP '   /qoff                   - stop quiz';
	print CLIENTCRAP 'Available settings:';
	print CLIENTCRAP '   quiz_type               : '.sprintf('%-5u', Irssi::settings_get_int('quiz_type')).' - quiz type (1 - dizzy, 2 - mieszacz)';
	print CLIENTCRAP '   quiz_delay              : '.sprintf('%-5u', Irssi::settings_get_int('quiz_delay')).' - delay between questions (sec)';
	print CLIENTCRAP '   quiz_anticheat_delay    : '.sprintf('%-5u', Irssi::settings_get_int('quiz_anticheat_delay')).' - hint/remind protection delay (sec; 0 - off)';
	print CLIENTCRAP '   quiz_points_per_answer  : '.sprintf('%-5u', Irssi::settings_get_int('quiz_points_per_answer')).' - points for correct answer';
	print CLIENTCRAP '   quiz_random_hints       : '.sprintf('%-5s', Irssi::settings_get_bool('quiz_random_hints') ? 'true' : 'false').' - reveal random letters or from left to right (dizzy alike)?';
	print CLIENTCRAP '   quiz_show_first_hint    : '.sprintf('%-5s', Irssi::settings_get_bool('quiz_show_first_hint') ? 'true' : 'false').' - show hint just after question?';
	print CLIENTCRAP '   quiz_first_hint_dots    : '.sprintf('%-5s', Irssi::settings_get_bool('quiz_first_hint_dots') ? 'true' : 'false').' - show first hint as dots only?';
	print CLIENTCRAP '   quiz_mix_on_remind      : '.sprintf('%-5s', Irssi::settings_get_bool('quiz_mix_on_remind') ? 'true' : 'false').' - scramble letters every remind (mieszacz)?';

	print CLIENTCRAP '   quiz_cmd_hint           : '.sprintf('%-5s', Irssi::settings_get_bool('quiz_cmd_hint') ? 'true' : 'false').' - is !hint command enabled?';
	print CLIENTCRAP '   quiz_cmd_remind         : '.sprintf('%-5s', Irssi::settings_get_bool('quiz_cmd_remind') ? 'true' : 'false').' - is !remind command enabled?';
}

##### Timers' events #####
sub evt_delayed_show_msg {
	Irssi::signal_emit('message own_public', $quiz{chanrec}{server}, shift, $quiz{chanrec}{name});
}

sub evt_next_question {
	($quiz{qtime}, $quiz{hnum}) = (time(), 0);
	$quiz{qnum}++;
	my $type = Irssi::settings_get_int('quiz_type');
	if ($type == 1) {
		$question = $quiz[($quiz{qnum} - 1) * 2];
		$answer = $quiz[($quiz{qnum} - 1) * 2 + 1];
		send_msg('!quiz_msg_question', [$quiz{qnum}, int(($#quiz + 1) / 2), $question]);
	} else {
		$answer = $quiz[$quiz{qnum} - 1];
		$question = confuse($answer);
		send_msg('!quiz_msg_question_2', [$quiz{qnum}, $#quiz + 1, $question]);
	}
	undef @dots;
	undef @hwords;
	@words = split(/ /, $answer);
	send_msg('quiz_msg_hint', [make_hint()]) if (Irssi::settings_get_bool('quiz_show_first_hint'));
	$quiz{inq} = 1;
}

##### Signals' handlers #####
sub sig_pubmsg {
	my ($server, $msg, $nick, $addr, $target) = @_;
	return if (!$quiz{ison} || $server->{tag} ne $quiz{chanrec}{server}{tag} || lc $target ne lc $quiz{chanrec}{name});
	if (lc $msg =~ /^!ile( ([^ ]+))?/) {
		my $who = $2;
		if ($who) {
			my $found = 0;
			foreach my $it (keys %nicks) {
				if (lc $nicks{$it} eq lc $who) {
					send_msg('quiz_msg_scorex', [$nicks{$it}, $score{$it}, ord_score($score{$it})]);
					$found = 1;
					last;
				}
			}
			send_msg('quiz_msg_noscorex', [$who]) if (!$found);
		} else {
			if ($score{$addr}) {
				send_msg('quiz_msg_score', [$nick, $score{$addr}, ord_score($score{$addr})]);
			} else {
				send_msg('quiz_msg_noscore', [$nick]);
			}
		}
	}
	return if (!$quiz{inq});
	if ((lc $msg eq '!podp') && Irssi::settings_get_bool('quiz_cmd_hint')) {
		if (!$quiz{hprot}) {
			send_msg('quiz_msg_hint', [make_hint()]);
			my $delay = Irssi::settings_get_int('quiz_anticheat_delay');
			if ($delay) {
				$quiz{hprot} = 1;
				$quiz{thint} = Irssi::timeout_add_once($delay * 1000, sub { $quiz{hprot} = 0; }, undef);
			}
		}
	} elsif ((lc $msg eq '!przyp') && Irssi::settings_get_bool('quiz_cmd_remind')) {
		if (Irssi::settings_get_int('quiz_type') == 2) {
			if (!$quiz{rprot}) {
				$question = confuse($answer) if (Irssi::settings_get_bool('quiz_mix_on_remind'));
				send_msg('quiz_msg_remind_2', [$question]);
				my $delay = Irssi::settings_get_int('quiz_anticheat_delay');
				if ($delay && Irssi::settings_get_bool('quiz_mix_on_remind')) {
					$quiz{rprot} = 1;
					$quiz{tremind} = Irssi::timeout_add_once($delay * 1000, sub { $quiz{rprot} = 0; }, undef);
				}
			}
		} else {
			send_msg('quiz_msg_remind', [$question]);
		}
	} elsif (lc $msg eq lc $answer) {
		stop_quiz();
		correct_answer($nick, $addr);
	}
}

##### Bindings #####
Irssi::command_bind('qon',		'cmd_start');
Irssi::command_bind('qstat',	'cmd_stat');
Irssi::command_bind('qdelay',	'cmd_delay');
Irssi::command_bind('qskip',	'cmd_skip');
Irssi::command_bind('qhint',	'cmd_hint');
Irssi::command_bind('qoff',		'cmd_stop');
Irssi::command_bind('quiz',		'cmd_help');

##### User settings #####
Irssi::settings_add_int($IRSSI{name},	'quiz_delay', 7);
Irssi::settings_add_int($IRSSI{name},	'quiz_anticheat_delay', 3);
Irssi::settings_add_int($IRSSI{name},	'quiz_points_per_answer', 1);
Irssi::settings_add_int($IRSSI{name},	'quiz_type', 1);
Irssi::settings_add_bool($IRSSI{name},	'quiz_random_hints', 1);
Irssi::settings_add_bool($IRSSI{name},	'quiz_show_first_hint', 0);
Irssi::settings_add_bool($IRSSI{name},	'quiz_first_hint_dots', 1);
Irssi::settings_add_bool($IRSSI{name},	'quiz_mix_on_remind', 1);
Irssi::settings_add_bool($IRSSI{name},	'quiz_cmd_hint', 1);
Irssi::settings_add_bool($IRSSI{name},	'quiz_cmd_remind', 1);
