#!/usr/bin/perl
package IkiWiki::Plugin::poll;

use warnings;
use strict;
use IkiWiki 3.00;
use Encode;

sub import {
	hook(type => "getsetup",   id => "poll", call => \&getsetup);
	hook(type => "preprocess", id => "poll", call => \&preprocess);
	hook(type => "scan",       id => "poll", call => \&scan);
	hook(type => "sessioncgi", id => "poll", call => \&sessioncgi);
 }

my %pagenum;
sub getsetup () {
	return
	 plugin =>
		 { safe => 1
		 , rebuild => undef
		 , section => "widget"
		 };
 }
my $params_re
 = qr{
	(?>
		(?>(?:[^\[\]]|\[[^\[]|\][^\]])+)
	|
		(?'loop'
			\[\[
			(?>
				(?>(?:[^\[\]]|\[[^\[]|\][^\]])+)
			|
				(?&loop)
			)*
			\]\]
		)
	)*
 }x;
sub scan (@) {
	my %params = @_;
	my $page = $params{page};
	my $content = $params{content};
	my $prefix = $config{prefix_directives} ? "!poll" : "poll";
	my $type = IkiWiki::pagetype($pagesources{$page});
	if (defined $type and $type eq "mdwn") {
		my %polls = ();
		while ($content =~ m{(\\?)\[\[\Q$prefix\E(\s+id="([^"]*)")?\s+($params_re)\s*\]\]}gs) {
			my ($escape, $poll, $directive) = ($1, $3, $4);
			next if $escape;
			$poll = '' unless defined $poll;
			error("poll id=`$poll' must match (|[a-z][a-z0-9_-]*) on page=`$page'")
				unless $poll =~              m/^(|[a-z][a-z0-9_-]*)$/;
			my %poll = ();
			while ($directive =~ m/(^|\s+)(\d+)(="([^"]*)")?\s+"?([^"]*)"?/gs) {
				my ($unknown_votes, $known_votes, $choice) = ($2, $4, $5);
				my @known_votes = defined $known_votes ? split(/\s+/, $known_votes) : ();
					$poll{$choice} =
					 { unknown_votes => $unknown_votes
					 , known_votes => \@known_votes
					 };
				foreach my $user (@known_votes) {
					my $userpage = linkpage(($config{userdir}?$config{userdir}.'/':'').$user);
					add_link($page, $userpage);
				 }
			 }
			error("poll id=`$poll' already exists on page=`$page'")
				if exists $polls{$poll};
			$polls{$poll} = \%poll;
		 }
		$IkiWiki::pagestate{$page}{poll} = \%polls;
	 }
 }
sub preprocess (@) {
	my %params=
	 ( open => "yes"
	 , total => "yes"
	 , percent => "yes"
	 , expandable => "no"
	 , @_ );
	
	my $open=IkiWiki::yesno($params{open});
	my $showtotal=IkiWiki::yesno($params{total});
	my $showpercent=IkiWiki::yesno($params{percent});
	my $expandable=IkiWiki::yesno($params{expandable});
	my $num=++$pagenum{$params{page}}{$params{destpage}};
	
	my %choices;
	my @choices;
	my $total=0;
	while (@_) {
		my $unknown_votes = shift;
		my $known_votes = shift;
		next
			unless $unknown_votes =~ /^\d+$/;
		my @users = $known_votes ? split(/\s+/, $known_votes) : ();
		my $choice = shift;
		shift;
		my $tot = ($unknown_votes + @users);
		$choices{$choice} =
		 { unknown_votes => $unknown_votes
		 , users => \@users
		 , total => $tot
		 };
		push @choices, $choice;
		$total += $tot;
	 }
	use URI::Escape;
	my $uri_page = URI::Escape::uri_escape_utf8($params{page}, '^A-Za-z0-9\-\._~/');
	my $ret="";
	foreach my $choice (@choices) {
		if ($open && exists $config{cgiurl}) {
			# use POST to avoid robots
			$ret.="<form method=\"POST\" action=\"".IkiWiki::cgiurl()."\">\n";
		 }
		$ret.="<dt class='choice'>";
		my $percent = $total > 0 ? int($choices{$choice}{total} / $total * 100) : 0;
		my $votes = $choices{$choice}{total};
		$votes .= '/'.$total
			if $showtotal;
		$votes .= " ($percent%)"
			if $showpercent;
		if (@{$choices{$choice}{users}} > 0) {
			$votes .= " : ".join(', ', map {
					my $userpage = linkpage(($config{userdir}?$config{userdir}.'/':'').$_);
					htmllink($params{page}, $params{destpage}, '/'.$userpage, linktext => pagetitle($_))
				 } @{$choices{$choice}{users}});
			$votes .= " + ".$choices{$choice}{unknown_votes}." "
			 . ($choices{$choice}{unknown_votes} > 1 ? gettext("unknowns") : gettext("unknown"))
				if $choices{$choice}{unknown_votes};
		 }
		else {
			$votes .= " : ".($choices{$choice}{unknown_votes}." ".gettext("unknowns"))
				if $choices{$choice}{unknown_votes};
		 }
		if ($open && exists $config{cgiurl}) {
			my $choice_escaped = URI::Escape::uri_escape_utf8($choice, '^A-Za-z0-9\ \-\._~/');
			$ret.="<input type=\"hidden\" name=\"do\" value=\"poll\" />\n";
			$ret.="<input type=\"hidden\" name=\"num\" value=\"$num\" />\n";
			$ret.="<input type=\"hidden\" name=\"page\" value=\"$uri_page\" />\n";
			$ret.="<input type=\"hidden\" name=\"choice\" value=\"$choice_escaped\" />\n";
			$ret.="<input type=\"submit\" value=\"".gettext("vote")."\" />\n";
		 }
		$ret.="<span class='description'>$choice</span>";
		$ret.="</dt>";
		$ret.="<dd class='votes'>";
		$ret.=$votes;
		$ret.="<hr class='poll' align=left width=\"$percent%\"/>\n";
		if ($open && exists $config{cgiurl}) {
			$ret.="</form>\n";
		 }
		$ret.="</dd>\n";
	 }
	
	if ($expandable && $open && exists $config{cgiurl}) {
		$ret.="<dt>";
		$ret.="<form method=\"POST\" action=\"".IkiWiki::cgiurl()."\">\n";
		$ret.="<input type=\"hidden\" name=\"do\" value=\"poll\" />\n";
		$ret.="<input type=\"hidden\" name=\"num\" value=\"$num\" />\n";
		$ret.="<input type=\"hidden\" name=\"page\" value=\"$uri_page\" />\n";
		$ret.=gettext("Write in").": <input name=\"choice\" size=50 />\n";
		$ret.="<input type=\"submit\" value=\"".gettext("vote")."\" />\n";
		$ret.="</dt>\n";
		$ret.="<dd>";
		$ret.="</dd>\n";
		$ret.="</form>\n";
		$ret.="</p>\n";
	 }
	return "<dl class='poll'>$ret</dl>";
 }
sub sessioncgi ($$) {
	my $cgi=shift;
	my $session=shift;
	if (defined $cgi->param('do') && $cgi->param('do') eq "poll") {
		my $choice = Encode::decode_utf8(URI::Escape::uri_unescape(IkiWiki::possibly_foolish_untaint($cgi->param('choice'))));
		
		if (! defined $choice || not length $choice) {
			error("no choice specified");
		 }
		my $num=$cgi->param('num');
		if (! defined $num) {
			error("no num specified");
		 }
		my $page=Encode::decode_utf8(URI::Escape::uri_unescape(IkiWiki::possibly_foolish_untaint($cgi->param('page'))));
		if (! defined $page || ! exists $pagesources{$page}) {
			use Data::Dumper;
			error("bad page name");
		 }
		&IkiWiki::check_canedit($page, $cgi, $session);
		
		# Did they vote before? If so, let them change their vote,
		# and check for dups.
		my $choice_param="poll_choice_${page}_$num";
		my $oldchoice=$session->param($choice_param);
		#if (defined $oldchoice && $oldchoice eq $choice) {
		#	# Same vote; no-op.
		#	IkiWiki::redirect($cgi, urlto($page));
		#	exit;
		# }
		my $prefix=$config{prefix_directives} ? "!poll" : "poll";
		my $content=readfile(srcfile($pagesources{$page}));
		# Now parse the content, find the right poll,
		# and find the choice within it, and increment its number.
		# If they voted before, decrement that one.
		my $edit=sub {
			my $escape=shift;
			my $params=shift;
			return $params
				if $escape;
			if (--$num == 0) {
				my $vote = sub {
					my ($action, $unknown_votes, $known_votes) = @_;
					my $user = $session->param("name");
					my %users;
					foreach (split(/\s+/, $known_votes)) {
						$users{$_} = 1;
					 }
					if ($action eq 'add') {
						if (defined $user) {
							if (exists $users{$user} or (defined $oldchoice and $oldchoice eq $choice)) {
								delete $users{$user};
								$known_votes = join(' ', sort {lc $a <=> lc $b} (keys %users));
							 }
							else {
								$known_votes = join(' ', sort {lc $a <=> lc $b} ($user, keys %users));
							 }
						 }
						else {
							$unknown_votes += 1;
						 }
					 }
					elsif ($action eq 'del') {
						if (defined $user) {
							if (exists $users{$user}) {
								delete $users{$user};
								$known_votes = join(' ', sort {lc $a <=> lc $b} (keys %users));
							 }
						 }
						else {
							$unknown_votes = ($unknown_votes-1 >=0 ? $unknown_votes-1 : 0);
						 }
					 }
					return $unknown_votes.($known_votes?"=\"$known_votes\"":"")
				 };
				if ($params=~s/(^|\s+)(\d+)(="([^"]*)")?(\s+)"?\Q$choice\E"?(\s+|$)/$1.$vote->('add', $2, $4)."$5\"$choice\"".$6/es) {
				 }
				elsif ($params=~/expandable=(\w+)/
				    & &IkiWiki::yesno($1)) {
					$choice=~s/["\]\n\r]//g;
					$params.=" 1 \"$choice\""
						if length $choice;
				 }
				if (defined $oldchoice and not ($oldchoice eq $choice)) {
					$params=~s/(^|\s+)(\d+)(="([^"]*)")?(\s+)"?\Q$oldchoice\E"?(\s+|$)/$1.$vote->('del', $2, $4)."$5\"$oldchoice\"".$6/es;
				 }
			 }
			return "$params";
		 };
		my $id='';
		$content =~
		 s{
			(?<escape>\\?)
			\[\[\Q$prefix\E
				(?:\s+id="(?<id>[^"]*)")?
				(?<space_begin>\s+)
				(?<params>$params_re)
				(?<space_end>\s*)
			\]\]
			}
		 {$id=$+{id};
			$+{escape}
			.'[['.$prefix
				.($+{id} eq ''?'':'id="'.$+{id}.'"')
				.$+{space_begin}
				.$edit->($+{escape}, $+{params})
				.$+{space_end}
			.']]'
		 }egsx;
		
		# Store their vote, update the page, and redirect to it.
		writefile($pagesources{$page}, $config{srcdir}, $content);
		if (defined $oldchoice and $choice eq $oldchoice) {
			$session->param($choice_param, undef);
			# TOTRY: $session->clear($choice_param);
		 }
		else {
			$session->param($choice_param, $choice);
		 }
		IkiWiki::cgi_savesession($session);
		if ($config{rcs}) {
			IkiWiki::disable_commit_hook();
			IkiWiki::rcs_commit
			 ( file    => $pagesources{$page}
			 , message => "poll vote: id=$id: $choice"
			 , session => $session
			 , token   => IkiWiki::rcs_prepedit($pagesources{$page})
			 );
			IkiWiki::enable_commit_hook();
			IkiWiki::rcs_update();
		 }
		require IkiWiki::Render;
		IkiWiki::refresh();
		IkiWiki::saveindex();
		# Need to set cookie in same http response that does the redir.
		eval q{use CGI::Cookie};
		error($@) if $@;
		my $cookie = CGI::Cookie->new
		 ( -name  => $session->name
		 , -value => $session->id );
		print $cgi->redirect
		 ( -cookie => $cookie
		 , -url    => urlto($page) );
		exit;
	 }
 }
package IkiWiki::PageSpec;
	sub match_poll ($$;@) {
		my ($page, $match, %params) = @_;
		my $polls = $IkiWiki::pagestate{$page}{poll};
		if (defined $polls and %$polls) {
			my ($match_poll, $match_user, $match_choice) = $match =~ m/^id=(.*?) user=(.*?) choice=(.*?)$/;
			if (exists $polls->{$match_poll}) {
				my %poll = %{$polls->{$match_poll}};
				my $match_user_re   = IkiWiki::glob2re($match_user?$match_user:'*');
				my $match_choice_re = IkiWiki::glob2re($match_choice?$match_choice:'*');
				while (my ($choice, $data) = each %poll) {
					next unless $choice =~ $match_choice_re;
					if ($match_user eq '') {
						if ($data->{unknown_votes} > 0) {
							return IkiWiki::SuccessReason->new("unkown user has voted for choice=`$choice'", $page => $IkiWiki::DEPEND_CONTENT);
						 }
						else {
							return IkiWiki::FailReason->new("no unkown user has voted for choice=`$choice'", $page => $IkiWiki::DEPEND_CONTENT);
						 }
					 }
					else {
						foreach my $user (@{$data->{known_votes}}) {
							next unless $user =~ $match_user_re;
							return IkiWiki::SuccessReason->new("user=`$user' has voted for choice=`$choice'", $page => $IkiWiki::DEPEND_CONTENT);
						 }
					 }
				 }
				return IkiWiki::FailReason->new("no user=`$match_user' has voted for choice=`$match_choice'", $page => $IkiWiki::DEPEND_CONTENT);
			 }
			else {
				return IkiWiki::FailReason->new("no poll id=`$match_poll'", $page => $IkiWiki::DEPEND_CONTENT);
			 }
		 }
		else {
			return IkiWiki::FailReason->new("no poll", $page => $IkiWiki::DEPEND_CONTENT);
		 }
	 }

1;
