#! /usr/bin/perl
# Copyright (C) 2014 Julien Moutinho <julm+ikiwiki+events&autogeree.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License,
# or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.

package IkiWiki::Plugin::events;

use strict;
use warnings;
use IkiWiki 3.00;
use Time::Local;
use DateTime;
use CGI::FormBuilder;
#use Data::Dumper;

sub import {
	hook(type => "getsetup",   id => "events", call => \&getsetup);
	hook(type => "needsbuild", id => "events", call => \&needsbuild);
	hook(type => "preprocess", id => "events", call => \&preprocess);
	hook(type => "sessioncgi", id => "events", call => \&sessioncgi);
 }
sub getsetup () {
	return
	 { plugin =>
		 { safe => 1
		 , rebuild => undef
		 , section => "widget"
		 }
	 };
 }

my $now
 = DateTime->now
 ( time_zone => 'local'
 , locale    => $config{locale}
 )->set_time_zone('floating');
my @days    = ('01'..'31');
my @hours   = ('00'..'23');
my @minutes = ('00'..'59');

# update
sub set_rendering_expiration ($$) {
	my ($page, $timestamp) = @_;
	if (not exists  $pagestate{$page}{events}{expiration}
	or $timestamp < $pagestate{$page}{events}{expiration}) {
		my $time = DateTime->from_epoch
		 ( epoch     => $timestamp
		 , time_zone => 'UTC'
		 , locale    => $config{locale}
		 );
		#debug("events: set_rendering_expiration(): will refresh: page=".$page
		# . " after: date=".$time->strftime('%Y-%m-%d_%H-%M-%S'));
		$pagestate{$page}{events}{expiration} = $timestamp;
	 }
 }
sub set_next_rendering (%) {
	my %params = @_;
	if ($params{type} eq 'month'
	 and $params{focus}->year()  == $now->year()
	 and $params{focus}->month() == $now->month()) {
		# NOTE: calendar for current month, updates next day
		my $update = $params{focus}->clone;
		$update->set_hour(0);
		$update->set_minute(0);
		$update->set_second(0);
		$update->set_nanosecond(0);
		my $duration = DateTime::Duration->new(days => 1, end_of_month => 'limit');
		$update->add_duration($duration);
		set_rendering_expiration($params{destpage}, $update->epoch());
		#debug("events: will refresh current month: page=".$params{destpage}
		# . " after: date=".$update->strftime('%Y-%m-%d_%H-%M-%S'));
	 }
	elsif ($params{type} eq 'month'
	 and ((  $params{focus}->year()  == $now->year()
			 and $params{focus}->month()  > $now->month())
		 or    $params{focus}->year()   > $now->year())) {
		# NOTE: calendar for upcoming month, updates 1st of next month
		my $update = $params{focus}->clone;
		$update->set_day(1);
		$update->set_hour(0);
		$update->set_minute(0);
		$update->set_second(0);
		$update->set_nanosecond(0);
		set_rendering_expiration($params{destpage}, $update->epoch());
		#debug("events: will refresh upcoming month: page=".$params{destpage}
		# . " after: date=".$update->strftime('%Y-%m-%d_%H-%M-%S'));
	 }
	elsif ($params{type} eq 'day'
	 and ($params{focus}->year()     == $now->year()
			 and $params{focus}->month() == $now->month()
			 and $params{focus}->day()   == $now->day())) {
		# NOTE: calendar for current day, updates next day
		my $update = $params{focus}->clone;
		$update->set_hour(0);
		$update->set_minute(0);
		$update->set_second(0);
		$update->set_nanosecond(0);
		my $duration = DateTime::Duration->new(days => 1, end_of_month => 'limit');
		$update->add_duration($duration);
		set_rendering_expiration($params{destpage}, $update->epoch());
		#debug("events: will refresh current day: page=".$params{destpage}
		# . " after: date=".$update->strftime('%Y-%m-%d_%H-%M-%S'));
	 }
	elsif ($params{type} eq 'day'
	 and ((    $params{focus}->year()  == $now->year()
			 and ( $params{focus}->month()  > $now->month()
				 or ($params{focus}->month() == $now->month()
				 and $params{focus}->day()    > $now->day() ))
		 or      $params{focus}->year()   > $now->year()))) {
		# NOTE: calendar for upcoming day, updates that day
		my $update = $params{focus}->clone;
		$update->set_hour(0);
		$update->set_minute(0);
		$update->set_second(0);
		$update->set_nanosecond(0);
		set_rendering_expiration($params{destpage}, $update->epoch());
		#debug("events: will refresh upcoming day: page=".$params{destpage}
		# . " after: date=".$update->strftime('%Y-%m-%d_%H-%M-%S'));
	 }
 }
sub needsbuild (@) {
	my $needsbuild = shift;
	foreach my $page (keys %pagestate) {
		if (exists $pagestate{$page}{events}{expiration}) {
			if ($pagestate{$page}{events}{expiration} <= $now->epoch()) {
				# NOTE: force a rebuild so the calendar shows the current day
				push @$needsbuild, $pagesources{$page};
			 }
			if (exists $pagesources{$page}
			 and grep { $_ eq $pagesources{$page} } @$needsbuild) {
				# NOTE: remove state, will be re-added
				#       if the calendar is still there during the rebuild
				delete $pagestate{$page}{events};
			 }
		 }
	 }
	return $needsbuild;
 }

# render
sub date_of_page ($%) {
	my ($page, %params) = @_;
	my $dir = IkiWiki::dirname($page);
	my ($year, $month, $day, $hour, $hour_begin, $hour_end)
	 = $dir =~ m{
		.*?
		/(\d+)
		(?:/(01|02|03|04|05|06|07|08|09|10|11|12)
			(?:/(01|02|03|04|05|06|07|08|09|10|11|12|13|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31)
				(?:/(([0-2][0-9]h[0-5][0-9])
					(?:-([0-2][0-9]h[0-5][0-9]))?)
				)?
			)?
		)?
		$
	 }x;
	my $r =
	 { year       => $year
	 , month      => $month
	 , day        => $day
	 , hour       => $hour
	 , hour_begin => $hour_begin
	 , hour_end   => $hour_end
	 };
	#debug("date_of_page: dir=".$dir." ".Dumper($r));
	return $r;
 }
sub event_of_page ($%) {
	my ($event, $date, %params) = @_;
	my $title
	 = exists $pagestate{$event}{meta}{title}
	 ? $pagestate{$event}{meta}{title}
	 : pagetitle(IkiWiki::basename($event));
	my $hour
	 = $date->{hour};
	my $link
	 = htmllink
	 ( $params{page}
	 , $params{destpage}
	 , $event
	 , linktext => $title
	 , noimageinline => 1
	 , title => $title );
	my @tags
	 = sort {lc $a cmp lc $b}
	 (grep {
			not defined $params{tags}
			or pagespec_match($_, $params{tags})
		 }
		 (keys %{$IkiWiki::typedlinks{$event}{tag}}));
	@tags
	 = map {
		my $tag_best = bestlink($params{page}, $_);
		$tag_best = (length $tag_best > 0 ? $tag_best : bestlink($event, $_));
		my $tag = (length $tag_best > 0 ? $tag_best : $_);
		my $title
		 = exists $pagestate{$tag}{meta}{title}
		 ? $pagestate{$tag}{meta}{title}
		 : pagetitle(IkiWiki::basename($tag));
		my $link
		 = htmllink
		 ( $params{page}
		 , $params{destpage}
		 , $tag
		 , linktext => $title
		 , noimageinline => 1
		 , title => $title );
		add_depends($params{page}, $event, deptype('content'));
		#add_depends($params{page}, $tag, deptype('content'));
			# XXX: much too heavy :\ and midnight refresh may fix it anyway.
		my $class = qq{$tag};
		$class =~ s{[^a-zA-Z0-9-]}{_}g;
		{ class => "tag tag-$class"
		, link  => $link }
	 } @tags;
	my $base = IkiWiki::dirname($event);
	$base =~ s/[^a-zA-Z0-9-]/_/g;
	return
	 { hour => $hour
	 , page => $event
	 , date => $date
	 , link => $link
	 , tags => \@tags
	 , base => $base };
 }
sub events_of_pages ($%) {
	my ($pages, %params) = @_;
	my @day_events = ();
	my @hour_events = ();
	my $pagedir = sub { IkiWiki::basename(IkiWiki::dirname(shift)) };
	foreach my $page (@$pages) {
		my $date = date_of_page($page);
		if (defined $date->{hour}) {
			push @hour_events, {page=>$page, date=>$date};
		 }
		else {
			push @day_events, {page=>$page, date=>$date};
		 }
	 }
	return
	 map {event_of_page($_->{page}, $_->{date}, %params)}
	 ( (sort {lc IkiWiki::basename($a->{page}) cmp lc IkiWiki::basename($b->{page})} @day_events)
	 , (sort {
		my $r = $a->{date}->{hour} cmp $b->{date}->{hour};
		if ($r) { $r }
		else { IkiWiki::basename($a->{page}) cmp IkiWiki::basename($b->{page}) }
	 } @hour_events) );
 }
sub event_html ($$%) {
	my ($date, $format, %params) = @_;
	my $day   = sprintf("%02d", $date->day());
	my $month = sprintf("%02d", $date->month());
	my $year_html = $date->year();
	my $month_html
	 = $format->{month_name}
	 ? $date->month_name()
	 : $month;
	my $day_html
	 = $format->{day_name}
	 ? $date->day_name()." ".$date->day()
	 : $format->{day_abbr}
	 ? $date->day_abbr()." ".$date->day()
	 : $date->day();
	my $wday_class
	 = "wday-".($date->day_of_week() - 1)
	 . ( ($date->year()    == $now->year()
			and $date->month() == $now->month()
			and $date->day()   == $now->day())
		 ? " today"
		 : "" );
	my $new_html = "";
	if (not defined $params{new} or $params{new} ne 'no') {
		if ($format->{year}) {
			my $year_page
			 = sprintf
			 ( '%s/%d'
			 , $params{base}
			 , $date->year()
			 );
			add_depends($params{page}, $year_page, deptype("presence"));
			if (exists $pagesources{$year_page}) {
				$year_html
				 = htmllink
				 ( $params{page}
				 , $params{destpage}
				 , $year_page
				 , linktext => $year_html
				 , noimageinline => 1 );
			 }
		 }
		if ($format->{month}) {
			my $month_page
			 = sprintf
			 ( '%s/%d/%02d'
			 , $params{base}
			 , $date->year()
			 , $date->month()
			 );
			add_depends($params{page}, $month_page, deptype("presence"));
			if (exists $pagesources{$month_page}) {
				$month_html
				 = htmllink
				 ( $params{page}
				 , $params{destpage}
				 , $month_page
				 , linktext => $month_html
				 , noimageinline => 1 );
			 }
		 }
		if ($format->{day}) {
			my $day_page
			 = sprintf
			 ( '%s/%d/%02d/%02d'
			 , $params{base}
			 , $date->year()
			 , $date->month()
			 , $date->day()
			 );
			add_depends($params{page}, $day_page, deptype("presence"));
			if (exists $pagesources{$day_page}) {
				$day_html
				 = htmllink
				 ( $params{page}
				 , $params{destpage}
				 , $day_page
				 , linktext => $day_html
				 , noimageinline => 1 );
			 }
		 }
		unless ($params{nonew} or not $format->{new}) {
			$new_html
			 .= qq{<a class='new' href='}
			 . IkiWiki::cgiurl
				 ( base  => $params{base}
				 , ($date->day()
					 ?(day => $day):())
				 , (($date->month() or $date->day())
					 ?(month => $month):())
				 , (($date->year() or $date->month() or $date->day())
					 ?(year => $date->year()):())
				 , do    => 'newevent'
				 , page  => $params{destpage}
				 )
			 . qq{' rel='nofollow'>+</a>};
		 }
	 }
	return
	 { new   => $new_html
	 , day   => $day_html
	 , month => $month_html
	 , year  => $year_html
	 , wday  => $wday_class
	 };
 }
sub preprocess_day (@) {
	my %params = @_;
	my @pages
	 = pagespec_match_list
	 ( $params{page}
	 , $params{pages}
	 , deptype => deptype("presence")
		# NOTE: add presence dependencies to update calendar when pages are added/removed
	 );
	my $event_html
	 = event_html
	 ( $params{focus}
	 , {day=>1, day_name=>1, new => 1}
	 , %params );
	my @events
	 = map {
		my @tags
		 = map {"<li\n class='".$_->{class}."'>".$_->{link}."</li>"}
		 @{$_->{tags}};
		"<ul\n class='events'><li class='event event-$_->{base}'>"
		 . "<span class='head'>"
				 . (defined $_->{hour} ? "<span class='hour'>$_->{hour}</span>" : "")
			 . "<span class='link'>$_->{link}</span>"
		 . "</span>"
		 . "<ul\n class='tags'>".join("", @tags)."</ul>"
		 . "</li></ul>"
	 }
	 events_of_pages(\@pages, %params);
	return
	 "<table\n class='wday'>"
	 . "<thead><tr><th><span\n class='head'><span\n class='day'>"
			 . $event_html->{day}
			 . $event_html->{new}
		 . "</span></span></th></tr></thead>"
	 . "<tbody><tr><td\n class='wday $event_html->{wday}'>"
		 . join("", @events)
		 . "</td></tr></tbody>"
	 . "</table>";
 }
sub preprocess_month (@) {
	my %params = @_;
	my $one_day = DateTime::Duration->new(days => 1, end_of_month => 'limit');
	my $day = $params{focus}->clone->set_day(1);
	my $last_day
	  = DateTime->last_day_of_month
	 ( year  => $params{focus}->year()
	 , month => $params{focus}->month() )->day();
	
	my @pages
	 = pagespec_match_list
	 ( $params{page}
	 , $params{pages}
	 , deptype => deptype("presence")
		# NOTE: add presence dependencies to update calendar when pages are added/removed
	 );
	
	my %events_by_day = map {($_=>[])} (1 .. $last_day);
	foreach my $event (events_of_pages(\@pages, %params)) {
		push @{$events_by_day{$event->{date}->{day}}}, $event;
	 }
	
	my $t='<tr>';
	my $first_wday = $day->clone();
	my $last_wday  = ($params{week_start_day} + 6) % 7;
	while ($first_wday->day_of_week() - 1 != $params{week_start_day}) {
	 # NOTE: pad the begining
		$first_wday->subtract_duration($one_day);
		$t.="<td class='no-wday'></td>";
	 }
	my $month = $day->month();
	for (; $day->month() == $month; $day->add_duration($one_day)) {
		my $event_html
		 = event_html
		 ( $day
		 , {day=>1, day_abbr=>1, new => 1}
		 , %params );
		$t.= "<td class='wday $event_html->{wday}'>";
		my @events
		 = map {
			my @tags
			 = map {"<li\n class='".$_->{class}."'>".$_->{link}."</li>"}
			 @{$_->{tags}};
			"<li\n class='event event-$_->{base}'>"
			 . "<span class='head'>"
				 . (defined $_->{hour} ? "<span class='hour'>$_->{hour}</span>" : "")
				 . "<span class='link'>$_->{link}</span>"
			 . "</span>"
			 . "<ul class='tags'>".join("", @tags)."</ul>"
			 . "</li>\n"
		 }
		 @{$events_by_day{sprintf('%02d',$day->day())}};
		$t .=
		 "<span class='head'>"
			 . "<span class='day'>"
				 . $event_html->{day}
				 . $event_html->{new}
			 . "</span>"
		 . "</span>"
		 . "<ul class='events'>".join("", @events)."</ul>";
		$t.='</td>';
		if ($day->day_of_week() - 1 == $last_wday) {
			$t.="</tr>";
			$t.="<tr>"
				if ($day->day_of_month() < $last_day);
		 }
	 }
	while ($day->day_of_week() - 1 != $params{week_start_day}) {
	 # NOTE: pad the end
		$day->add_duration($one_day);
		$t.="<td class='no-wday'></td>";
	 }
	$t.='</tr>';
	my $event_html
	 = event_html
	 ( $params{focus}
	 , {year=>1, month=>1, month_name=>1}
	 , %params );
	return
	 "<table class='month'>"
	 . "<thead>"
		 . "<tr>"
		 . "<th colspan='7'>"
		 . "<span class='month'>$event_html->{month}</span>"
		 . " <span class='year'>$event_html->{year}</span>"
		 . "</th>"
		 . "</tr>"
		 . "<tr>"
		 . join ("", map {
			$_ = "<th><span>".$first_wday->day_name()."</span></th>";
			$first_wday->add_duration($one_day);
			$_ } (1..7))
		 . "</tr>"
	 . "</thead>"
	 . "<tbody>$t</tbody></table>";
 }
sub preprocess (@) {
	my %params = @_;
	$params{focus} = $now->clone;
	$params{focus}->set_hour(0);
	$params{focus}->set_minute(0);
	$params{focus}->set_second(0);
	$params{focus}->set_nanosecond(0);
	
	$params{pages} = "*"        unless defined $params{pages};
	$params{type}  = "month"    unless defined $params{type};
	$params{week_start_day} = 0 unless defined $params{week_start_day};
	$params{week_start_day} = $params{week_start_day} % 7;
	
	unless (defined $params{base}) {
		$params{base}
		 = defined $config{events_base}
		 ? $config{events_base}
		 : gettext('Agenda');
	 }
	
	my %focus_set;
	if (defined $params{day} and $params{day} =~ m/^(\d+)$/) {
		my ($day) = ($1);
		$focus_set{day} = $day;
	 }
	if (defined $params{month} and $params{month} =~ m/^(\d+)$/) {
		my ($month) = ($1);
		$focus_set{month} = $month;
	 }
	if (defined $params{year} and $params{year} =~ m/^(\d+)$/) {
		my ($year) = ($1);
		$focus_set{year} = $year;
	 }
	$params{focus}->set(%focus_set);
	
	if (defined $params{day} and $params{day} =~ m/^([+-])(\d+)$/) {
		my ($sign, $days) = ($1, $2);
		my $duration = DateTime::Duration->new(days => $days, end_of_month => 'limit');
		$params{focus}
		 = $sign eq '+'
		 ? $params{focus}->add_duration($duration)
		 : $params{focus}->subtract_duration($duration);
	 }
	if (defined $params{month} and $params{month} =~ m/^([+-])(\d+)$/) {
		my ($sign, $months) = ($1, $2);
		my $duration = DateTime::Duration->new(months => $months, end_of_month => 'limit');
		$params{focus}
		 = $sign eq '+'
		 ? $params{focus}->add_duration($duration)
		 : $params{focus}->subtract_duration($duration);
	 }
	if (defined $params{year} and $params{year} =~ m/^([+-])(\d+)$/) {
		my ($sign, $years) = ($1, $2);
		my $duration = DateTime::Duration->new(years => $years, end_of_month => 'limit');
		$params{focus}
		 = $sign eq '+'
		 ? $params{focus}->add_duration($duration)
		 : $params{focus}->subtract_duration($duration);
	 }
	
	#debug("events: focus=".$params{focus}->strftime('%Y-%m-%d_%H-%M-%S'));
	$params{pages} =~ s[%Y][$params{focus}->year()]eg;
	$params{pages} =~ s[%m][sprintf('%02d', $params{focus}->month())]eg;
	$params{pages} =~ s[%d][sprintf('%02d', $params{focus}->day())]eg;
	
	set_next_rendering(%params);
	
	my $calendar = "";
	if ($params{type} eq 'month') {
		$calendar = preprocess_month(%params);
	 }
	elsif ($params{type} eq 'day') {
		$calendar = preprocess_day(%params);
	 }
	
	return "<div class='calendar'>$calendar</div>";
 }

# new
sub tmpl ($$) {
	my ($base, $model) = @_;
	my $page = $base.'/'.'templates/'.$model;
	my $file = defined srcfile($page, 1) ? '/'.$page : $model;
	return template($file);
 }
sub date_of_form ($$;%) {
	my ($form, $prefix, %default) = @_;
	%default =
	 ( year   => 0
	 , month  => 1
	 , day    => 1
	 , hour   => 0
	 , minute => 0
	 , %default
	 );
	my $date;
	eval { $date = DateTime->new
	 ( year       => ($form->field($prefix.'_year')   ne '' ? $form->field($prefix.'_year')                : $default{year})
	 , month      => ($form->field($prefix.'_month')  ne '' ? substr($form->field($prefix.'_month'), 0, 2) : $default{month})
	 , day        => ($form->field($prefix.'_day')    ne '' ? $form->field($prefix.'_day')                 : $default{day})
	 , hour       => ($form->field($prefix.'_hour')   ne '' ? $form->field($prefix.'_hour')                : $default{hour})
	 , minute     => ($form->field($prefix.'_minute') ne '' ? $form->field($prefix.'_minute')              : $default{minute})
	 , second     => 0
	 , nanosecond => 0
	 , time_zone  => 'local'
	 , locale     => $config{locale}
	 )->set_time_zone('floating') };
	return $date;
 };
sub duration_of_form ($$) {
	my ($form, $prefix) = @_;
	my $dur;
	eval { $dur = DateTime::Duration->new
	 ( years        => $form->field($prefix.'_year')
	 , months       => $form->field($prefix.'_month')
	 , days         => $form->field($prefix.'_day')
	 , weeks        => $form->field($prefix.'_week')
	 , hours        => $form->field($prefix.'_hour')
	 , minutes      => $form->field($prefix.'_minute')
	 , seconds      => 0
	 , nanoseconds  => 0
	 , end_of_month => 'limit'
	 ) };
	return $dur;
 };
sub page_of_event ($$$$$) {
	my ($form, $from_date, $to_date, $name, $base) = @_;
	my $time = '';
	if ($form->field('from_hour')   ne ''
	or  $form->field('from_minute') ne '') {
		if ($from_date->hour() == $to_date->hour()
		 and $from_date->minute() == $to_date->minute()) {
			$time = sprintf('%02dh%02d', $from_date->hour(), $from_date->minute());
		 }
		else {
			$time = sprintf('%02dh%02d-%02dh%02d'
			 , $from_date->hour(), $from_date->minute()
			 , $to_date->hour(), $to_date->minute());
		 }
	 }
	return
	 ( $base
	 . ($base?'/':'').$from_date->year()
	 . '/'.sprintf('%02d', $from_date->month())
	 . '/'.sprintf('%02d', $from_date->day())
	 . '/'. ($time ne '' ? $time . '/' : '')
	 . $name
	 );
 }
sub check_cannewevent ($$$$) {
	my $dest=shift;
	my $destfile=shift;
	my $cgi=shift;
	my $session=shift;
	
	# Must be a legal filename.
	if (IkiWiki::file_pruned($destfile)) {
		error(sprintf(gettext("illegal name")));
	 }
	# Must not be a known source file.
	if (exists $pagesources{$dest}) {
		error(sprintf(gettext("%s already exists"),
			htmllink("", "", $dest
			 , linktext => $dest
			 , noimageinline => 1)));
	 }
	# Must not exist on disk already.
	if (-l "$config{srcdir}/$destfile" || -e _) {
		error(sprintf(gettext("%s already exists on disk"), $destfile));
	 }
	
	# Must be editable.
	IkiWiki::check_canedit($dest, $cgi, $session);
	
	my $can_newevent;
	IkiWiki::run_hooks(can_newevent => sub {
		return if defined $can_newevent;
		my $ret=shift->(cgi => $cgi, session => $session, dest => $dest, destfile => $destfile);
		if (defined $ret) {
			if ($ret eq "") {
				$can_newevent=1;
			 }
			elsif (ref $ret eq 'CODE') {
				$ret->();
				$can_newevent=0;
			 }
			elsif (defined $ret) {
				error($ret);
				$can_newevent=0;
			 }
		 }
	 });
	return defined $can_newevent ? $can_newevent : 1;
 }
sub post_newevent ($$$) {
	my $cgi=shift;
	my $session=shift;
	my $dest=shift;
	
	IkiWiki::redirect($cgi, urlto($dest));
	exit;
 }
sub newevent_hook {
	my %params  = @_;
	my @events  = @{$params{events}};
	my %done    = %{$params{done}};
	my $cgi     = $params{cgi};
	my $session = $params{session};
	return ()
		unless @events;
	my @next;
	foreach my $event (@events) {
		unless (exists $done{$event->{page}} && $done{$event->{file}}) {
			IkiWiki::run_hooks(newevent => sub {
				push @next, shift->
				 ( cgi     => $cgi
				 , event   => $event
				 , session => $session
				 );
			 });
			$done{$event->{page}} = 1;
		 }
	 }
	push @events, newevent_hook
	 ( cgi     => $cgi
	 , done    => \%done
	 , events  => \@next
	 , session => $session
	 );
	my %seen; # NOTE: insure unicity
	return grep { ! $seen{$_->{page}}++ } @events;
 }
sub preview($$$$$) {
	my ($cgi, $session, $form, $events, $months) = @_;
	$form->tmpl_param(year   => gettext("year"));
	$form->tmpl_param(month  => gettext("month"));
	$form->tmpl_param(day    => gettext("day"));
	$form->tmpl_param(hour   => gettext("hour"));
	$form->tmpl_param(min    => gettext("min"));
	$form->tmpl_param(dow    => gettext("day of week"));
	$form->tmpl_param(page   => gettext("page"));
	$form->tmpl_param(events => [
		map {
			{ from_year      => $_->{from}->year()
			, from_month     => sprintf('%02d', $_->{from}->month())
			, from_monthname => $months->{$_->{from}->month()}
			, from_day       => sprintf('%02d', $_->{from}->day())
			, from_hour      => sprintf('%02d', $_->{from}->hour())
			, from_minute    => sprintf('%02d', $_->{from}->minute())
			, from_dow       => $_->{from}->dow()
			, from_downame   => $_->{from}->day_name()
			, to_year        => $_->{to}->year()
			, to_month       => sprintf('%02d', $_->{to}->month())
			, to_monthname   => $months->{$_->{to}->month()}
			, to_day         => sprintf('%02d', $_->{to}->day())
			, to_hour        => sprintf('%02d', $_->{to}->hour())
			, to_minute      => sprintf('%02d', $_->{to}->minute())
			, to_dow         => $_->{to}->dow()
			, page           =>
				htmllink("", "", $_->{page}
				 , linktext => $_->{page}
				 , noimageinline => 1)
			}
		 } @$events
	 ]);
	if (@$events > 0) {
		my $page = @$events[0];
		# FROM: editpage.pm
		my $new = not exists $pagesources{$page};
		# temporarily record its type
		my $type = $config{default_pageext};
		$pagesources{$page} = $page.".".$type if $new;
		my %wasrendered = map { $_ => 1 } @{$renderedfiles{$page}};
		my $content = @$events[0]->{content};
		
		IkiWiki::run_hooks(editcontent => sub {
			$content = shift->
			 ( cgi     => $cgi
			 , content => $content
			 , page    => $page
			 , session => $session
			 );
		 });
		my $preview = IkiWiki::htmlize($page, $page, $type,
			IkiWiki::linkify($page, $page,
			IkiWiki::preprocess($page, $page,
			IkiWiki::filter($page, $page, $content), 0, 1)));
		IkiWiki::run_hooks(format => sub {
			$preview = shift->
			 ( content => $preview
			 , page    => $page
			 );
		 });
		$form->tmpl_param("preview", $preview);
		
		# Previewing may have created files on disk.
		# Keep a list of these to be deleted later.
		my %previews = map { $_ => 1 } @{$wikistate{editpage}{previews}};
		foreach my $f (@{$renderedfiles{$page}}) {
			$previews{$f} = 1 unless $wasrendered{$f};
		 }
		
		# Throw out any other state changes made during previewing,
		# and save the previews list.
		IkiWiki::loadindex();
		@{$wikistate{editpage}{previews}} = keys %previews;
		IkiWiki::saveindex();
	 }
	else {
		$form->tmpl_param("preview", gettext("No event"));
	 }
 }
sub create ($$$$$) {
	my ($event, $cgi, $session, $months, $base) = @_;
	check_cannewevent
	 ( $event->{page}
	 , $event->{file}
	 , $cgi
	 , $session
	 );
	my $pageext = $config{default_pageext};
	
	$config{cgi} = 0; # NOTE: avoid CGI error message
	eval { writefile($event->{file}, $config{srcdir}, $event->{content}) };
	if ($config{rcs}) {
		IkiWiki::rcs_add($event->{file});
	 }
	# month page
		my $monthpage =
		 ( $base
		 . ($base?'/':'').$event->{from}->year()
		 . '/'.sprintf('%02d', $event->{from}->month())
		 );
		my $monthfile = IkiWiki::newpagefile($monthpage, $pageext);
		if (not exists $pagesources{$monthpage}
		 and not -l $config{srcdir}.'/'.$monthfile
		 and not -e _) {
			my $tmpl_neweventmonth = tmpl($base, 'neweventmonth.tmpl');
			$tmpl_neweventmonth->param(base      => $base);
			$tmpl_neweventmonth->param(year      => $event->{from}->year());
			$tmpl_neweventmonth->param(month     => sprintf('%02d', $event->{from}->month()));
			$tmpl_neweventmonth->param(monthname => $months->{$event->{from}->month()});
			my $content = $tmpl_neweventmonth->output();
			eval { writefile($monthfile, $config{srcdir}, $content) };
			if ($config{rcs}) {
				IkiWiki::rcs_add($monthfile);
			 }
		 }
	# day page
		my $daypage =
		 ( $monthpage
		 . '/'.sprintf('%02d', $event->{from}->day())
		 );
		my $dayfile = IkiWiki::newpagefile($daypage, $pageext);
		if (not exists $pagesources{$daypage}
		 and not -l $config{srcdir}.'/'.$dayfile
		 and not -e _) {
			my $tmpl_neweventday = tmpl($base, 'neweventday.tmpl');
			$tmpl_neweventday->param(base      => $base);
			$tmpl_neweventday->param(year      => $event->{from}->year());
			$tmpl_neweventday->param(month     => sprintf('%02d', $event->{from}->month()));
			$tmpl_neweventday->param(monthname => $months->{$event->{from}->month()});
			$tmpl_neweventday->param(day       => sprintf('%02d', $event->{from}->day()));
			$tmpl_neweventday->param(dayname   => $event->{from}->day_name());
			my $content = $tmpl_neweventday->output();
			eval { writefile($dayfile, $config{srcdir}, $content) };
			if ($config{rcs}) {
				IkiWiki::rcs_add($dayfile);
			 }
		 }
	$config{cgi} = 1;
 }
sub sessioncgi ($$) {
	my ($cgi, $session) = @_;
	if (defined $cgi->param('do') && $cgi->param('do') eq "newevent") {
		# TOTRY: decode_cgi_utf8($cgi);
		my $base = Encode::decode_utf8(URI::Escape::uri_unescape(IkiWiki::possibly_foolish_untaint($cgi->param('base'))));
		&IkiWiki::check_canedit($base, $cgi, $session);
		my $page = Encode::decode_utf8(URI::Escape::uri_unescape(IkiWiki::possibly_foolish_untaint($cgi->param('page'))));
		
		my $now_date = DateTime->now
		 ( time_zone => 'local'
		 , locale    => $config{locale}
		 )->set_time_zone('floating');
		my %dows   = map { ($_ => $now_date->{locale}->day_format_wide->[ $_ ]) } (0..6);
		my %months = map { ($_ => $now_date->{locale}->month_format_wide->[ $_ - 1 ]) } (1..12);
		my $cgi_date;
		eval { $cgi_date = DateTime->new
		 ( year       => defined $cgi->param("year")   ? $cgi->param("year")   : $now_date->year()
		 , month      => defined $cgi->param("month")  ? $cgi->param("month")  : $now_date->month()
		 , day        => defined $cgi->param("day")    ? $cgi->param("day")    : $now_date->day()
		 , hour       => defined $cgi->param("hour")   ? $cgi->param("hour")   : $now_date->hour()
		 , minute     => defined $cgi->param("minute") ? $cgi->param("minute") : $now_date->minute()
		 , second     => 0
		 , nanosecond => 0
		 , time_zone  => 'local'
		 , locale     => $config{locale}
		 )->set_time_zone('floating') };
		error(sprintf(gettext("illegal date")))
			unless $cgi_date;
		
		my @years = ($cgi_date->year() .. $cgi_date->year()+5);
		my $week_start_day
		 = (defined $config{week_start_day} and $config{week_start_day} >= 0 and $config{week_start_day} <= 6)
		 ?          $config{week_start_day}
		 : 1;
		my @dow_order = ($week_start_day .. 6, 0 .. $week_start_day-1);
		
		my $tags = $typedlinks{$page}{tag};
		my $buttons = [qw{Preview Create}];
		my ($from_date, $to_date, $end_date, $inc_dur);
		my $form = CGI::FormBuilder->new
		 ( action => IkiWiki::cgiurl()
		 , charset => "utf-8"
		 , fields => [qw{
			 do base
			 from_date from_year from_month from_day from_hour from_minute
			 to_date to_year to_month to_day to_hour to_minute
			 inc_dur inc_year inc_month inc_week inc_day inc_hour inc_minute
			 end_times end_date end_year end_month end_day end_hour end_minute
			 dom name content
			 }]
		 , header => 0
		 , javascript => 0
		 , messages =>
			 {
			#   form_required_text  => 'form_required_text'
			# , form_invalid_text   => 'form_invalid_text'
			# , form_invalid_file   => 'form_invalid_file'
			# , form_invalid_input  => gettext('allowed characters: ').$config{wiki_file_chars}
			 form_invalid_select => gettext('invalid selection')
			 }
		 , method     => 'POST'
		 , name       => "newevent"
		 , stylesheet => 1
		 , params     => $cgi
		 , required   => [qw{do base year month day name from_date to_date end_date inc_dur}]
		 , submit     => [qw{Preview Create}]
		 , title      => gettext("newevent")
		 , template   => { template("newevent.tmpl") }
		 , validate   =>
			 { from_date => { perl => sub {
					my (undef, $form) = @_;
					$from_date = date_of_form($form, 'from')
						unless defined $from_date;
					defined $from_date
				 } }
			 , to_date   => { perl => sub {
					my (undef, $form) = @_;
					$from_date = date_of_form($form, 'from')
						unless defined $from_date;
					if (defined $from_date) {
						$to_date = date_of_form($form, 'to'
						 , year   => $from_date->year()
						 , month  => $from_date->month()
						 , day    => $from_date->day()
						 , hour   => $from_date->hour()
						 , minute => $from_date->minute());
						defined $to_date
						and (DateTime->compare($from_date, $to_date) <= 0)
					 }
					else {return 0;}
				 } }
			 , end_date  => { perl => sub {
					my (undef, $form) = @_;
					if ( $form->field('end_year')   ne ''
					 or  $form->field('end_month')  ne ''
					 or  $form->field('end_day')    ne '' ) {
						$from_date = date_of_form($form, 'from')
							unless defined $from_date;
						if (defined $from_date) {
							$end_date = date_of_form($form, 'end'
							 , year   => $from_date->year()
							 , month  => $from_date->month()
							 , day    => $from_date->day()
							 , hour   => $from_date->hour()
							 , minute => $from_date->minute());
							(defined $from_date and defined $end_date
							and DateTime->compare($from_date, $end_date) <= 0)
						 }
						else {return 0;}
					 }
					else {
						1;
					 }
				 } }
			 , name => '/^.+$/'
			 , base => '/^.*$/'
			 , end_times  => sub { $_[0] =~ m/^\d+$/ and $_[0] >= 0 }
			 , inc_year   => sub { $_[0] =~ m/^\d+$/ and $_[0] >= 0 }
			 , inc_month  => sub { $_[0] =~ m/^\d+$/ and $_[0] >= 0 }
			 , inc_week   => sub { $_[0] =~ m/^\d+$/ and $_[0] >= 0 }
			 , inc_day    => sub { $_[0] =~ m/^\d+$/ and $_[0] >= 0 }
			 , inc_hour   => sub { $_[0] =~ m/^\d+$/ and $_[0] >= 0 }
			 , inc_minute => sub { $_[0] =~ m/^\d+$/ and $_[0] >= 0 }
			 , inc_dur    => sub {
					my (undef, $form) = @_;
					$inc_dur = duration_of_form($form, 'inc');
					defined $inc_dur
					and ($inc_dur->is_positive() or $inc_dur->is_zero());
				 }
			 }
		 );
		$base = $form->field('base') ? $form->field('base') : $base;
		$form->title(sprintf(gettext("creating new events"), pagetitle(IkiWiki::basename($page))));
		$form->field(name => "do", type => "hidden", value => 'newevent', force => 1);
		$form->field(name => "base", type => "hidden", force => 1 , value => $base);
		$form->field(name => "from_date", type => "hidden", value => '1', force => 1);
		$form->field(name => "to_date", type => "hidden", value => '1', force => 1);
		$form->field(name => "end_date", type => "hidden", value => '1', force => 1);
		$form->field(name => "inc_dur", type => "hidden", value => '1', force => 1);
		$form->field(name => "from_year", type => 'select', value => $cgi_date->year(), options => \@years);
		$form->field(name => "from_month", type => 'select'
		 , value => sprintf("%02d", $cgi_date->month()).' - '.$months{$cgi_date->month()}
		 , options => [map { sprintf("%02d", $_).' - '.$months{$_} } (1..12)]);
		$form->field(name => "from_day", type => 'select'
		 , value => sprintf("%02d", $cgi_date->day())
		 , options => \@days);
		$form->field(name => "from_hour", type => 'select', value => '', options => \@hours);
		$form->field(name => "from_minute", type => 'select', value => '', options => \@minutes);
		$form->field(name => "name", type => 'text', size => 60, value => gettext('New event'));
		$form->field(name => "to_year", type => 'select', value => '', options => \@years);
		$form->field(name => "to_month", type => 'select'
		 , value => ''
		 , options => [map { sprintf("%02d", $_).' - '.$months{$_} } (1..12)]);
		$form->field(name => "to_day", type => 'select'
		 , value => ''
		 , options => \@days);
		$form->field(name => "to_hour", type => 'select', value => '', options => \@hours);
		$form->field(name => "to_minute", type => 'select', value => '', options => \@minutes);
		$form->field(name => "end_year", type => 'select', value => '', options => \@years);
		$form->field(name => "end_month", type => 'select', value => ''
		 , options => [map { sprintf("%02d", $_).' - '.$months{$_} } (1..12)]);
		$form->field(name => "end_day", type => 'select', value => '', options => \@days);
		$form->field(name => "end_hour", type => 'select', value => '', options => \@hours);
		$form->field(name => "end_minute", type => 'select', value => '', options => \@minutes);
		$form->field(name => "end_times", type => 'text', value => '0', size => 2);
		$form->field(name => "inc_year", type => 'text', value => '0', size => 2);
		$form->field(name => "inc_month", type => 'text', value => '0', size => 2);
		$form->field(name => "inc_week", type => 'text', value => '0', size => 2);
		$form->field(name => "inc_day", type => 'text', value => '0', size => 2);
		$form->field(name => "inc_hour", type => 'text', value => '0', size => 2);
		$form->field(name => "inc_minute", type => 'text', value => '0', size => 2);
		my $tmpl_neweventcontent = tmpl($base, 'neweventcontent.tmpl');
		$tmpl_neweventcontent->param(title => gettext('Title of the event'));
		$tmpl_neweventcontent->param(tags => [map {{name => $_}} (sort keys %$tags)]);
		$form->field(name => "content", type => "textarea", size => 30, rows => 20, cols => 80
		 , value => $tmpl_neweventcontent->output());
		$form->field(name => "dom", type => 'select', multiple => 1, size => 35
		 , options => [map { my $n = $_; map {($n.' '.$dows{$_})} (0..6)} ('1°', '2°', '3°', '4°', '5°')]);
		
		IkiWiki::decode_form_utf8($form);
		IkiWiki::run_hooks(formbuilder_setup => sub {
			shift->(form => $form, cgi => $cgi, session => $session, buttons => $buttons);
		 });
		IkiWiki::decode_form_utf8($form);
		
		if (($form->submitted eq 'Create' || $form->submitted eq 'Preview') && $form->validate) {
			#IkiWiki::checksessionexpiry($cgi, $session, $cgi->param('sid'));
			$base
			 = $form->field('base')
			 ? $form->field('base')
			 : (defined $config{base} ? $config{base} : gettext('Agenda'));
			my $end_times
			 = $form->field('end_times') == 0
			 ? undef : $form->field('end_times');
			my $dom;
			foreach ($form->field('dom')) {
				$dom = {} if not defined $dom;
				$dom->{$_} = 1;
			 }
			my $name = $form->field('name');
			$name = IkiWiki::possibly_foolish_untaint(IkiWiki::titlepage($name));
				# NOTE: these untaints are safe because of the checks
				#       performed in check_cannewevent later.
			my $content = $form->field('content');
			$content =~ s/\r\n/\n/gs;
			$content =~ s/\n$//s;
			
			# Queue of event creations to perfom.
			my @events = ();
			my $events_try = 0;
			my $events_max
			 = defined $config{newevent_max_per_commit}
			 ? $config{newevent_max_per_commit} : (2 * 365) ;
			my $pageext = $config{default_pageext};
			while (++$events_try <= $events_max
			 and (not defined $end_times or --$end_times >= 0)
			 and (not defined $end_date or DateTime->compare($from_date, $end_date) <= 0)) {
				my $dest = page_of_event($form, $from_date, $to_date, $name, $base);
				my $week = $from_date->weekday_of_month();
				my $day = $now_date->{locale}->day_format_wide->[$from_date->day_of_week()-1];
				if (not defined $dom or exists $dom->{"$week° $day"}) {
					push @events,
					 { page => $dest
					 , file => IkiWiki::newpagefile($dest, $pageext)
					 , from => $from_date
					 , to   => $to_date
					 , name => $name
					 };
				 }
				last unless defined $inc_dur and $inc_dur->is_positive();
				$from_date = $from_date->clone->add_duration($inc_dur);
				$to_date   = $to_date->clone->add_duration($inc_dur);
			 }
			error("events try per commit overflow: $events_max")
				unless $events_try <= $events_max;
			my $tmpl_neweventpage = tmpl($base, 'neweventpage.tmpl');
			my $i = 0;
			foreach (@events) {
				$tmpl_neweventpage->clear_params();
				$tmpl_neweventpage->param(content => $content);
				$tmpl_neweventpage->param(page => $_->{page});
				$tmpl_neweventpage->param(event => $i);
				$tmpl_neweventpage->param("event_first" => 1)
					if $i == 0;
				$tmpl_neweventpage->param("event_last" => 1)
					if $i == @events - 1;
				$tmpl_neweventpage->param(events => \@events);
				$tmpl_neweventpage->param(from_date => "$_->{from}");
				$tmpl_neweventpage->param(name => $_->{name});
				$tmpl_neweventpage->param(to_date => "$_->{to}");
				$_->{content} = $tmpl_neweventpage->output();
				$i++;
			 }
			if ($form->submitted eq 'Create') {
				@events = newevent_hook
				 ( cgi     => $cgi
				 , done    => {}
				 , events  => \@events
				 , session => $session
				 );
				require IkiWiki::Render;
				if ($config{rcs}) {
					IkiWiki::disable_commit_hook()
				 }
				foreach my $event (@events) {
					create($event, $cgi, $session, \%months, $base);
				 }
				if ($config{rcs}) {
					IkiWiki::rcs_commit_staged
					 ( message => sprintf(gettext("new event"))
					 , session => $session );
					IkiWiki::enable_commit_hook();
					IkiWiki::rcs_update();
				 }
				IkiWiki::refresh();
				IkiWiki::saveindex();
				
				post_newevent($cgi, $session, (defined $events[0] ? $events[0]->{page} : ''));
			 }
			elsif ($form->submitted eq 'Preview') {
				preview($cgi, $session, $form, \@events, \%months);
				IkiWiki::showform($form, $buttons, $session, $cgi);
			 }
		 }
		else {
			IkiWiki::showform($form, $buttons, $session, $cgi);
		 }
		
		exit 0;
	 }
 }

1;
