  
# DateTags.pl
# Movable Type plugin tags for date-based output
# by Kevin Shay
# http://www.staggernation.com/mtplugins/
# last modified February 17, 2005

use strict;
package MT::Plugin::DateTagsA;
use vars qw( $VERSION );
$VERSION = '2.3';

use MT;
use MT::Template::Context;

eval{ require MT::Plugin };
unless ($@) {
    my $plugin = {
        name => "DateTags $VERSION",
        description => 'Enhance your MT site with a variety of date-related tags.',
        doc_link => 'http://www.staggernation.com/mtplugins/DateTags'
    }; 
    MT->add_plugin(new MT::Plugin($plugin));
}

MT::Template::Context->add_tag('EnglishOrdinal' => sub{&_hdlr_english_ordinal;});
MT::Template::Context->add_tag('DeltaDays' => sub{&_hdlr_delta_days;});
MT::Template::Context->add_conditional_tag('IfDaysOfWeek' => sub{&_hdlr_if_days_of_week;});
MT::Template::Context->add_conditional_tag('IfDaysOfMonth' => sub{&_hdlr_if_days_of_month;});
MT::Template::Context->add_conditional_tag('IfMonths' => sub{&_hdlr_if_months;});
MT::Template::Context->add_conditional_tag('IfDateWithin' => sub{&_hdlr_if_date_within;});
MT::Template::Context->add_conditional_tag('IfDateEqual' => sub{&_hdlr_if_date_equal;});
MT::Template::Context->add_conditional_tag('IfDateNotEqual' => sub{&_hdlr_if_date_not_equal;});
MT::Template::Context->add_conditional_tag('IfDateBefore' => sub{&_hdlr_if_date_before;});
MT::Template::Context->add_conditional_tag('IfDateAfter' => sub{&_hdlr_if_date_after;});
MT::Template::Context->add_conditional_tag('IfDateBeforeOrEqual' => sub{&_hdlr_if_date_before_or_equal;});
MT::Template::Context->add_conditional_tag('IfDateAfterOrEqual' => sub{&_hdlr_if_date_after_or_equal;});
MT::Template::Context->add_container_tag('DateLoop' => sub{&_hdlr_date_loop;});
MT::Template::Context->add_tag('DateLoopDate' => sub{&_hdlr_date_loop_date;});
MT::Template::Context->add_container_tag('DateRange' => sub{&_hdlr_date_range;});
MT::Template::Context->add_tag('DateRangeStartDate' => sub{&_hdlr_date_range_start_date;});
MT::Template::Context->add_tag('DateRangeEndDate' => sub{&_hdlr_date_range_end_date;});
MT::Template::Context->add_conditional_tag('IfEntries' => sub{&_hdlr_if_entries;});
MT::Template::Context->add_conditional_tag('IfNoEntries' => sub{&_hdlr_if_no_entries;});
MT::Template::Context->add_container_tag('NextNEntries' => sub{&_hdlr_next_n_entries;});
MT::Template::Context->add_container_tag('PrevNEntries' => sub{&_hdlr_prev_n_entries;});
MT::Template::Context->add_conditional_tag('IfNEntries' => sub{&_hdlr_if_n_entries;});
MT::Template::Context->add_tag('NEntriesCount' => sub{&_hdlr_n_entries_count;});
	# for backwards compatibility
MT::Template::Context->add_conditional_tag('DateLoopIfEntries' => sub{&_hdlr_if_entries;});
MT::Template::Context->add_conditional_tag('DateLoopIfNoEntries' => sub{&_hdlr_if_no_entries;});

use DateTags;
use MT::Util qw ( format_ts );
	# keep track of whether we've loaded Date::Calc
my $datecalc = 0;

sub dt_load_datecalc {
# instead of use()ing Date::Calc, load it the first time it's needed
# (so it'll only load if one of the tags is actually called)
	require Date::Calc;
	import Date::Calc qw( Date_to_Days Day_of_Week Delta_Days English_Ordinal
			check_date Add_Delta_Days );
	return 1;
}

sub _hdlr_english_ordinal {
	my ($ctx, $args) = @_;
	$datecalc ||= dt_load_datecalc();
	(my $number = $args->{'number'})
		|| return $ctx->error('No number passed.');
	$number = dt_build_value($ctx, $number);
	return English_Ordinal($number);
}

sub _hdlr_if_days_of_week {
	my ($ctx, $args, $cond) = @_;
	$datecalc ||= dt_load_datecalc();
	my $builder = $ctx->stash('builder');
	my $tokens = $ctx->stash('tokens');
	return $ctx->error('No days passed') unless defined($args->{'days'});
	my ($y, $m, $d) = unpack('A4A2A2', dt_get_ts($ctx, $args));
	my $dow = Day_of_Week($y, $m, $d);
	return 0 unless ($args->{'days'} =~ /$dow/);
	my $proceed = 1;
	if (defined($args->{'n'})) {
		$proceed = 0;
		for (split(/[ ,]/, $args->{'n'})) {
			my $nwomy = dt_nwomy($_, $dow, $m, $y);
			if ($nwomy == $d) {
				$proceed = 1;
				last;
			}
		}
	}
	return $proceed;
}

sub _hdlr_if_days_of_month {
	my ($ctx, $args, $cond) = @_;
	my $builder = $ctx->stash('builder');
	my $tokens = $ctx->stash('tokens');
	return $ctx->error('No days passed') unless defined($args->{'days'});
	my $d = substr(dt_get_ts($ctx, $args), 6, 2);
	my %days = map { sprintf("%02d", $_) => 1 } split(/[ ,]/, $args->{'days'});
	return $days{$d} ? 1 : 0;
}

sub _hdlr_if_months {
	my ($ctx, $args, $cond) = @_;
	my $builder = $ctx->stash('builder');
	my $tokens = $ctx->stash('tokens');
	return $ctx->error('No months passed') unless defined($args->{'months'});
	my $m = substr(dt_get_ts($ctx, $args), 4, 2);
	my %months = map { sprintf("%02d", $_) => 1 } split(/[ ,]/, $args->{'months'});
	return $months{$m} ? 1 : 0;
}

sub _hdlr_if_date_within {
	my ($ctx, $args, $cond) = @_;
	$datecalc ||= dt_load_datecalc();
	my $builder = $ctx->stash('builder');
	my $tokens = $ctx->stash('tokens');
	my (%now, %start, %end);
	@now{qw(y m d)} = unpack('A4A2A2', dt_get_ts($ctx, $args));
	dt_get_start_end($ctx, $args, \%start, \%end);
	(Delta_Days(@start{qw(y m d)}, @end{qw(y m d)}) < 0)
		&& return $ctx->error('Ending date before starting date');
	my $within = ((dt_days(\%now) >= dt_days(\%start))
		&& (dt_days(\%now) <= dt_days(\%end)));
	return ($within xor defined($args->{'not'})) ? 1 : 0;
}

sub _hdlr_delta_days {
	my ($ctx, $args) = @_;
	$datecalc ||= dt_load_datecalc();
	my $delta = dt_delta_days($ctx, $args);
	if (defined($args->{'adjust'})) {
		$delta += $args->{'adjust'};
	}
	if (($delta < 0) && !defined($args->{'negative'})) {
		$delta = abs($delta);
	}
	my $thou = defined($args->{'thousands'}) ? $args->{'thousands'} : '';
	if (defined($args->{'plural'})) {
		return (abs($delta) == 1) ? '' : $args->{'plural'};	
	} elsif (defined($args->{'ordinal'})) {
		my $ord = English_Ordinal($delta);
		if ($thou) {
				# we want "1,234th"
			return dt_insert_commas($delta, $thou) . substr($ord, length($ord) - 2);
		} else {
			return $ord;
		}
	} else {
		return $thou ? dt_insert_commas($delta, $thou) : $delta;
	}
}

sub _hdlr_if_date_equal {
	my ($ctx, $args, $cond) = @_;
	return dt_date_compare_hdlr($ctx, $args, $cond, '==');
}

sub _hdlr_if_date_not_equal {
	my ($ctx, $args, $cond) = @_;
	return dt_date_compare_hdlr($ctx, $args, $cond, '!=');
}

sub _hdlr_if_date_before {
	my ($ctx, $args, $cond) = @_;
	return dt_date_compare_hdlr($ctx, $args, $cond, '>');
}

sub _hdlr_if_date_after {
	my ($ctx, $args, $cond) = @_;
	return dt_date_compare_hdlr($ctx, $args, $cond, '<');
}

sub _hdlr_if_date_before_or_equal {
	my ($ctx, $args, $cond) = @_;
	return dt_date_compare_hdlr($ctx, $args, $cond, '>=');
}

sub _hdlr_if_date_after_or_equal {
	my ($ctx, $args, $cond) = @_;
	return dt_date_compare_hdlr($ctx, $args, $cond, '<=');
}

sub _hdlr_next_n_entries {
	return dt_n_entries('ascend', @_);
}

sub _hdlr_prev_n_entries {
	return dt_n_entries('descend', @_);
}

sub dt_n_entries {
	my ($direction, $ctx, $args, $cond) = @_;
	return $ctx->error('No "n" value passed') unless ($args->{'n'});
	require MT::Entry;
	my $builder = $ctx->stash('builder');
	my $tokens = $ctx->stash('tokens');
	my $ts = dt_get_ts($ctx, $args);
	my $column = $args->{'modified_on'} ? 'modified_on' : 'created_on';
	my @entries = MT::Entry->load({ blog_id => $ctx->stash('blog')->id,
		status => MT::Entry::RELEASE() }, 
		{ sort => $column,
		  start_val => $ts,
		  direction => $direction,
		  limit => $args->{'n'} });
	local $ctx->{__stash}{'entries'} = \@entries;
	local $ctx->{__stash}{'datetags_n_entries'} = scalar @entries;
	defined(my $text = $builder->build($ctx, $tokens, $cond))
		|| return $ctx->error($ctx->errstr);

	return $text;
}

sub _hdlr_if_n_entries {
	my ($ctx, $args, $cond) = @_;
	return '' unless $ctx->stash('datetags_n_entries');
	defined(my $text = $ctx->stash('builder')->build($ctx, $ctx->stash('tokens'), $cond))
		|| return $ctx->error($ctx->errstr);
	return $text;
}

sub _hdlr_n_entries_count {
	my ($ctx, $args, $cond) = @_;
	return $ctx->error('Not called from within MTNextNEntries or MTPrevNEntries container')
		unless defined(my $count = $ctx->stash('datetags_n_entries'));
	return $count;
}

sub _hdlr_date_loop {
	my ($ctx, $args, $cond) = @_;
	$datecalc ||= dt_load_datecalc();
	require MT::Entry;
	my $builder = $ctx->stash('builder');
	my $tokens = $ctx->stash('tokens');
	my (%start, %end);
	dt_get_start_end($ctx, $args, \%start, \%end);
	my $delta = Delta_Days(@start{qw(y m d)}, @end{qw(y m d)});
	my $text = '';
	my $i = 0;
		# loop through dates. we need to loop at least once, and include
		# both the start and end dates.
	{
		my $ymd = sprintf("%04d%02d%02d", Add_Delta_Days(@start{qw(y m d)}, $i));
		my $start_ts = "${ymd}000000";
		my $end_ts = "${ymd}235959";
		my @entries = MT::Entry->load({ blog_id => $ctx->stash('blog')->id,
			status => MT::Entry::RELEASE(), created_on => [ $start_ts, $end_ts ] }, 
			{ range => { created_on => 1 } });
			# workaround for MT 2.x bug when loading entries by range under DBM
		@entries = map { (substr($_->created_on, 0, 8) eq $ymd) ? $_ : () } @entries;
		local $ctx->{__stash}{'dateloop_ts'} = $start_ts;
		local $ctx->{__stash}{'entries'} = \@entries;
		defined(my $iter = $builder->build($ctx, $tokens, $cond))
			|| return $ctx->error($ctx->errstr);
		$text .= $iter;
		last if ($i == $delta);
		($delta > 0) ? $i++ : $i--;
		redo;
	}
	return $text;

}

sub _hdlr_date_loop_date {
	my ($ctx, $args) = @_;
	(my $ts = $ctx->stash('dateloop_ts'))
		|| return $ctx->error('Not called from within MTDateLoop container');
	return format_ts($args->{'format'}, $ts, $ctx->stash('blog'), $args->{'language'});
}

sub _hdlr_date_range {
	my ($ctx, $args, $cond) = @_;
	$datecalc ||= dt_load_datecalc();
	require MT::Entry;
	my $builder = $ctx->stash('builder');
	my $tokens = $ctx->stash('tokens');
	my (%start, %end);
	dt_get_start_end($ctx, $args, \%start, \%end);
	my $start_ts = sprintf("%04d%02d%02d000000", @start{qw(y m d)});
	my $end_ts = sprintf("%04d%02d%02d235959", @end{qw(y m d)});
	my @entries = MT::Entry->load({ blog_id => $ctx->stash('blog')->id,
		status => MT::Entry::RELEASE(), created_on => [ $start_ts, $end_ts ] }, 
		{ range => { created_on => 1 } });
		# workaround for MT 2.x bug when loading entries by range under DBM
	@entries = map { ($_->created_on le $end_ts) ? $_ : () } @entries;
	local $ctx->{__stash}{'daterange_start_ts'} = $start_ts;
	local $ctx->{__stash}{'daterange_end_ts'} = $end_ts;
	local $ctx->{__stash}{'entries'} = \@entries;
	defined(my $text = $builder->build($ctx, $tokens, $cond))
		|| return $ctx->error($ctx->errstr);
	return $text;
}

sub _hdlr_date_range_start_date {
	my ($ctx, $args) = @_;
	(my $ts = $ctx->stash('daterange_start_ts'))
		|| return $ctx->error('Not called from within MTDateRange container');
	return format_ts($args->{'format'}, $ts, $ctx->stash('blog'), $args->{'language'});
}

sub _hdlr_date_range_end_date {
	my ($ctx, $args) = @_;
	(my $ts = $ctx->stash('daterange_end_ts'))
		|| return $ctx->error('Not called from within MTDateRange container');
	return format_ts($args->{'format'}, $ts, $ctx->stash('blog'), $args->{'language'});
}

sub _hdlr_if_entries {
	my ($ctx, $args, $cond) = @_;
	return dt_if_entries($ctx, $args, $cond, 1);
}

sub _hdlr_if_no_entries {
	my ($ctx, $args, $cond) = @_;
	return dt_if_entries($ctx, $args, $cond, 0);
}

sub dt_if_entries {
	my ($ctx, $args, $cond, $want_entries) = @_;
	my $entries = $ctx->stash('entries');
	my $has_entries = ($entries && @$entries) ? 1 : 0;
	return '' unless ($has_entries == $want_entries);
	defined(my $text = $ctx->stash('builder')->build($ctx, $ctx->stash('tokens'), $cond))
		|| return $ctx->error($ctx->errstr);
	return $text;
}

sub dt_get_start_end {
	my ($ctx, $args, $start, $end) = @_;
	my %flds = ('start' => $start, 'end' => $end);
	for (keys %flds) {
		return $ctx->error("No $_ date passed") unless ($args->{$_});
		(my $ts = dt_build_date($ctx, $args, $args->{$_}))
			|| return $ctx->error("Invalid $_ date");
		@{$flds{$_}}{qw(y m d)} = unpack('A4A2A2', $ts);
		if (my $adj = $args->{"${_}_adjust"}) {
			@{$flds{$_}}{qw(y m d)} = Add_Delta_Days(@{$flds{$_}}{qw(y m d)}, $adj);
		}
	}

}

sub dt_date_compare_hdlr {
	my ($ctx, $args, $cond, $op) = @_;
	my $builder = $ctx->stash('builder');
	my $tokens = $ctx->stash('tokens');
	my $text = '';
	my $delta = dt_delta_days($ctx, $args);
	return eval("$delta $op 0") ? 1 : 0;
}

sub dt_delta_days {
	my ($ctx, $args) = @_;
	$datecalc ||= dt_load_datecalc();
	return $ctx->error('No target date passed') unless $args->{'target'};
	my (%date, %target);
	check_date(@date{qw(y m d)} = unpack('A4A2A2', dt_get_ts($ctx, $args)))
		|| return $ctx->error('Invalid base date');
	check_date(@target{qw(y m d)} = unpack('A4A2A2', dt_build_date($ctx, $args, $args->{'target'})))
		|| return $ctx->error('Invalid target date');
	return Delta_Days(@date{qw(y m d)}, @target{qw(y m d)});
}

sub dt_days {
# passed a ref to a date hash with keys (y, m, d),
# return the absolute "days" (since 1/1/1) value for that date
	my ($date) = @_;
	return Date_to_Days(@{$date}{qw(y m d)});
}

sub dt_insert_commas {
# put commas in a large number
	my ($num, $comma) = @_;
	return $num if (abs($num) < 1000);
	$comma ||= ',';
	$num = reverse($num);
	$num =~ s/(\d{3})(?=\d)(?!\d*\.)/$1$comma/g;
	$num = scalar reverse($num);
	return $num;
}

1;
