# Tagwire Plugin (aka AllKeywords Plugin)
# a plugin for listing and handling "tags"
#
# $Id$
#
# This software is provided as-is. You may use it for commercial or 
# personal use. If you distribute it, please keep this notice intact.
#
# Copyright (c) 2005 Hirotaka Ogawa

package MT::Plugin::Tagwire;
use strict;
use MT::Template::Context;
use MT::Request;
use vars qw($VERSION);

$VERSION = '0.24';

# DEBUG
my $FORCE_PD_REFRESH = 0;
my $ENABLE_PD_INDEXES = 1;
my $ENABLE_REQ_CACHE = 1;

my $plugin;
eval {
    require MT::Plugin;
    $plugin = new MT::Plugin({
	name => 'Tagwire Plugin',
	description => 'A plugin for listing and handling blog-wide tags and entry tags.',
	doc_link => 'http://as-is.net/hacks/2005/06/tagwire_plugin.html',
	author_name => 'Hirotaka Ogawa',
	author_link => 'http://profile.typekey.com/ogawa/',
	version => $VERSION
	});
    MT->add_plugin($plugin);
};

if (MT->can('add_callback')) {
    my $mt = MT->instance;
    MT->add_callback((ref $mt eq 'MT::App::CMS' ? 'AppPostEntrySave' : 'MT::Entry::post_save'),
		     10, $plugin, \&update_pd_indexes);
}

sub update_pd_indexes {
    return unless $ENABLE_PD_INDEXES && $plugin;
    my ($eh, $app, $entry) = @_;
    require MT::Entry;
    my $blog_id = $entry->blog_id;
    require MT::PluginData;
    my $pd = MT::PluginData->load({ plugin => $plugin->name,
				    key => $blog_id });
    my (%eindex, %tindex);
    my $data;
    my $refresh = $FORCE_PD_REFRESH || 0;
    if (!$pd) {
	$pd = new MT::PluginData();
	$pd->plugin($plugin->name);
	$pd->key($blog_id);
	$refresh = 1;
    }
    $data = $pd->data() || {};
    $refresh = 1 if !exists $data->{version} || ${$data->{version}} ne $VERSION;
    if ($refresh) {
	my $iter = MT::Entry->load_iter({ blog_id => $blog_id,
					  status => MT::Entry::RELEASE() });
	while (my $e = $iter->()) {
	    my @tags = split_tags($e->keywords, 1) or next;
	    $eindex{$e->id} = { tags => \@tags,
				created_on => $e->created_on };
	}
    } else {
	my $eid = $entry->id;
	%eindex = %{$data->{eindex}};
	delete $eindex{$eid} if exists $eindex{$eid};
	if ($entry->status == MT::Entry::RELEASE()) {
	    my @tags = split_tags($entry->keywords, 1);
	    $eindex{$eid} = { tags => \@tags,
			      created_on => $entry->created_on };
	}
    }
    foreach my $eid (keys %eindex) {
	my $ts = $eindex{$eid}->{created_on};
	foreach (@{$eindex{$eid}->{tags}}) {
	    push @{$tindex{$_}->{eids}}, $eid;
	    $tindex{$_}->{ts} = $ts if $tindex{$_}->{ts} < $ts;
	}
    }
    $data->{version} = \$VERSION;
    $data->{eindex} = \%eindex;
    $data->{tindex} = \%tindex;
    $pd->data($data);
    $pd->save or die $pd->errstr;
    if ($ENABLE_REQ_CACHE) {
	my $r = MT::Request->instance;
	$r->cache('Tagwire::Cache::' . $blog_id, undef);
    }
}

MT::Template::Context->add_container_tag('Tags' => \&tags);
MT::Template::Context->add_container_tag('EntryTags' => \&entry_tags);
MT::Template::Context->add_container_tag('RelatedTags' => \&related_tags);
MT::Template::Context->add_tag('Tag' => \&tag);
MT::Template::Context->add_tag('TagCount' => \&tag_count);
MT::Template::Context->add_tag('TagDate' => \&tag_date);
MT::Template::Context->add_tag('TagsTotal' => \&tags_total);
MT::Template::Context->add_tag('TagsTotalSum' => \&tags_total_sum);
MT::Template::Context->add_container_tag('EntriesWithTags' => \&entries);
MT::Template::Context->add_container_tag('MostRelatedEntries' => \&most_related_entries);
MT::Template::Context->add_global_filter('encode_urlplus' => \&encode_urlplus);

# For compatibility (this plugin was formerly named 'AllKeywords')
MT::Template::Context->add_container_tag('AllKeywords' => \&tags);
MT::Template::Context->add_container_tag('EntryAllKeywords' => \&entry_tags);
MT::Template::Context->add_tag('AllKeyword' => \&tag);
MT::Template::Context->add_tag('AllKeywordCount' => \&tag_count);
MT::Template::Context->add_tag('AllKeywordsTotal' => \&tags_total);
MT::Template::Context->add_tag('AllKeywordsTotalSum' => \&tags_total_sum);
MT::Template::Context->add_container_tag('EntriesWithKeywords' => \&entries);

sub split_args {
    my ($string, $delimiter, $case_sensitive) = @_;
    return unless $string;
    $string =~ s/(^\s+|\s+$)//g;
    $string = lc $string unless $case_sensitive;

    return split(/\s+/, $string) unless $delimiter;

    my @tags;
    foreach my $tag (split($delimiter, $string)) {
	$tag =~ s/(^\s+|\s+$)//g;
	push @tags, $tag if $tag;
    }
    @tags;
}

sub split_tags {
    my ($string, $case_sensitive) = @_;
    return unless $string;
    my @tags;
    $string =~ s/(^\s+|\s+$)//g;
    $string = lc $string unless $case_sensitive;
#    $string =~ s/\[[^[]+\]//g; # uncomment this to discard [short title]

    if ($string =~ m/[;,|]/) {
	# tags separated by non-whitespaces
	while ($string =~ m/(\[[^]]+\]|"[^"]+"|'[^']+'|[^;,|]+)/g) {
	    my $tag = $1;
	    $tag =~ s/(^[\["'\s;,|]+|[\]"'\s;,|]+$)//g;
	    push @tags, $tag if $tag;
	}
    } else {
	# tags separated by whitespaces
	while ($string =~ m/(\[[^]]+\]|"[^"]+"|'[^']+'|[^\s]+)/g) {
	    my $tag = $1;
	    $tag =~ s/(^[\["'\s]+|[\]"'\s]+$)//g;
	    push @tags, $tag if $tag;
	}
    }
    @tags;
}

sub get_pd_indexes {
    return unless $ENABLE_PD_INDEXES && $plugin;
    my $blog_id = $_[0] or return;
    my ($r, $cname);
    if ($ENABLE_REQ_CACHE) {
	$r = MT::Request->instance;
	$cname = 'Tagwire::Cache::' . $blog_id;
	return $r->cache($cname) if defined $r->cache($cname);
	$r->cache($cname, undef);
    }
    my $data;
    eval {
	require MT::PluginData;
	my $pd = MT::PluginData->load({ plugin => $plugin->name,
					key => $blog_id });
	$data = $pd->data() if $pd;
    };
    return if !exists $data->{version} || ${$data->{version}} ne $VERSION;
    $r->cache($cname, $data) if $ENABLE_REQ_CACHE && $data;
    $data;
}

sub get_db_indexes {
    my $blog_id = $_[0] or return;
    my ($r, $cname);
    if ($ENABLE_REQ_CACHE) {
	$r = MT::Request->instance;
	$cname = 'Tagwire::Cache::' . $blog_id;
	return $r->cache($cname) if defined $r->cache($cname);
	$r->cache($cname, undef);
    }
    my $data;
    my (%eindex, %tindex);
    my $iter = MT::Entry->load_iter({ blog_id => $blog_id,
				      status => MT::Entry::RELEASE() });
    while (my $e = $iter->()) {
	my @tags = split_tags($e->keywords, 1) or next;
	$eindex{$e->id} = { tags => \@tags,
			    created_on => $e->created_on };
    }
    foreach my $eid (keys %eindex) {
	my $ts = $eindex{$eid}->{created_on};
	foreach (@{$eindex{$eid}->{tags}}) {
	    push @{$tindex{$_}->{eids}}, $eid;
	    $tindex{$_}->{ts} = $ts if $tindex{$_}->{ts} < $ts;
	}
    }
    $data->{eindex} = \%eindex;
    $data->{tindex} = \%tindex;
    $r->cache($cname, $data) if $ENABLE_REQ_CACHE;
    $data;
}

sub tags {
    my ($ctx, $args, $cond) = @_;

    # sort_by (tag/tag-case/count, default = tag)
    my $sort_by = $args->{sort_by} || 'tag';
    # sort_order (ascend/descend, default = ascend)
    my $sort_order = $args->{sort_order} || 'ascend';
    # lastn (default = 0, no cutoff)
    my $lastn = $args->{lastn} || 0;
    # case_sensitive (0/1, default = 1)
    my $case_sensitive = defined $args->{case_sensitive} ?
	$args->{case_sensitive} : 1;

    my $blog_id = $ctx->stash('blog_id');
    my %tags = ();
    my %ts = ();

    my $data = get_pd_indexes($blog_id) || get_db_indexes($blog_id)
	or return '';
    my %tindex = %{$data->{tindex}};
    if ($case_sensitive) {
	foreach (keys %tindex) {
	    $tags{$_} = scalar @{$tindex{$_}->{eids}};
	    $ts{$_} = $tindex{$_}->{ts};
	}
    } else {
	foreach (keys %tindex) {
	    $tags{lc $_} += scalar @{$tindex{$_}->{eids}};
	    $ts{lc $_} = $tindex{$_}->{ts} if $ts{lc $_} < $tindex{$_}->{ts};
	}
    }

    my @list;
    if ($sort_by eq 'tag' || $sort_by eq 'keyword' ) {
	@list = $sort_order eq 'ascend' ?
	    sort { lc $a cmp lc $b } keys %tags :
	    sort { lc $b cmp lc $a } keys %tags;
    } elsif ($sort_by eq 'tag-case' || $sort_by eq 'keyword-case') {
	@list = $sort_order eq 'ascend' ?
	    sort keys %tags :
	    sort reverse keys %tags;
    } else {
	@list = $sort_order eq 'ascend' ?
	    sort { $tags{$a} <=> $tags{$b} } keys %tags :
	    sort { $tags{$b} <=> $tags{$a} } keys %tags;
    }

    $ctx->stash('Tagwire::tags_total', scalar @list);

    my $total_sum = 0;
    $total_sum += $tags{$_} foreach (@list);
    $ctx->stash('Tagwire::tags_total_sum', $total_sum);

    my @res;
    my $builder = $ctx->stash('builder');
    my $tokens = $ctx->stash('tokens');
    my $i = 0;
    foreach (@list) {
	last if $lastn && $i >= $lastn;
	local $ctx->{__stash}{'Tagwire::tag'} = $_;
	local $ctx->{__stash}{'Tagwire::tag_count'} = $tags{$_};
	local $ctx->{__stash}{'Tagwire::tag_date'} = $ts{$_};
	defined(my $out = $builder->build($ctx, $tokens))
	    or return $ctx->error($ctx->errstr);
	push @res, $out;
	$i++;
    }
    my $glue = $args->{glue} || '';
    join $glue, @res;
}

sub entry_tags {
    my ($ctx, $args, $cond) = @_;
    my $e = $ctx->stash('entry')
	or return $ctx->_no_entry_error('MT' . $ctx->stash('tag'));
    return '' unless $e->keywords;

    # case_sensitive (0/1, default = 1)
    my $case_sensitive = defined $args->{case_sensitive} ?
	$args->{case_sensitive} : 1;

    my @tags = split_tags($e->keywords, $case_sensitive);
    my @res;
    my $builder = $ctx->stash('builder');
    my $tokens = $ctx->stash('tokens');
    foreach (@tags) {
	local $ctx->{__stash}{'Tagwire::tag'} = $_;
	defined(my $out = $builder->build($ctx, $tokens))
	    or return $ctx->error($ctx->errstr);
	push @res, $out;
    }
    my $glue = $args->{glue} || '';
    join $glue, @res;
}

sub related_tags {
    my ($ctx, $args, $cond) = @_;
    my $tag = $ctx->stash('Tagwire::tag') or return '';

    my $sort_by = $args->{sort_by} || 'tag';
    # sort_order (ascend/descend, default = ascend)
    my $sort_order = $args->{sort_order} || 'ascend';
    # lastn (default = 0, no cutoff)
    my $lastn = $args->{lastn} || 0;
    # case_sensitive (0/1, default = 1)
    my $case_sensitive = defined $args->{case_sensitive} ?
	$args->{case_sensitive} : 1;

    my $blog_id = $ctx->stash('blog_id');
    my %tags = ();
    my %ts = ();

    my $data = get_pd_indexes($blog_id) || get_db_indexes($blog_id)
	or return '';
    my %tindex = %{$data->{tindex}};
    my %eindex = %{$data->{eindex}};
    if ($case_sensitive) {
	foreach my $eid (@{$tindex{$tag}->{eids}}) {
	    foreach (@{$eindex{$eid}->{tags}}) {
		next if $_ eq $tag;
		$tags{$_} = exists $tags{$_} ? $tags{$_} + 1 : 1;
		$ts{$_} = $tindex{$_}->{ts} if !defined $ts{$_};
	    }
	}
    } else {
	$tag = lc $tag;
	foreach my $nctag (grep { lc $_ eq $tag } keys %tindex) {
	    foreach my $eid (@{$tindex{$nctag}->{eids}}) {
		foreach (@{$eindex{$eid}->{tags}}) {
		    my $mtag = lc $_;
		    next if $mtag eq $tag;
		    $tags{$mtag} = exists $tags{$mtag} ? $tags{$mtag} + 1 : 1;
		    $ts{$mtag} = $tindex{$_}->{ts} if $ts{$mtag} < $tindex{$_}->{ts};
		}
	    }
	}
    }
    my @list;
    if ($sort_by eq 'tag' || $sort_by eq 'keyword' ) {
	@list = $sort_order eq 'ascend' ?
	    sort { lc $a cmp lc $b } keys %tags :
	    sort { lc $b cmp lc $a } keys %tags;
    } elsif ($sort_by eq 'tag-case' || $sort_by eq 'keyword-case') {
	@list = $sort_order eq 'ascend' ?
	    sort keys %tags :
	    sort reverse keys %tags;
    } else {
	@list = $sort_order eq 'ascend' ?
	    sort { $tags{$a} <=> $tags{$b} } keys %tags :
	    sort { $tags{$b} <=> $tags{$a} } keys %tags;
    }

    $ctx->stash('Tagwire::tags_total', scalar @list);

    my $total_sum = 0;
    $total_sum += $tags{$_} foreach (@list);
    $ctx->stash('Tagwire::tags_total_sum', $total_sum);

    my @res;
    my $builder = $ctx->stash('builder');
    my $tokens = $ctx->stash('tokens');
    my $i = 0;
    foreach (@list) {
	last if $lastn && $i >= $lastn;
	local $ctx->{__stash}{'Tagwire::tag'} = $_;
	local $ctx->{__stash}{'Tagwire::tag_count'} = $tags{$_};
	local $ctx->{__stash}{'Tagwire::tag_date'} = $ts{$_};
	defined(my $out = $builder->build($ctx, $tokens))
	    or return $ctx->error($ctx->errstr);
	push @res, $out;
	$i++;
    }
    my $glue = $args->{glue} || '';
    join $glue, @res;
}

sub tag {
    $_[0]->stash('Tagwire::tag') || '';
}

sub tag_count {
    $_[0]->stash('Tagwire::tag_count') || 0;
}

sub tag_date {
    my ($ctx, $args) = @_;
    $args->{ts} = $ctx->stash('Tagwire::tag_date')
	or return '';
    MT::Template::Context::_hdlr_date($ctx, $args);
}

sub tags_total {
    $_[0]->stash('Tagwire::tags_total') || 0;
}

sub tags_total_sum {
    $_[0]->stash('Tagwire::tags_total_sum') || 0;
}

sub entries {
    my ($ctx, $args, $cond) = @_;

    # tags(keywords) (REQUIRED)
    my $search = $args->{tags} || $args->{keywords} or return '';
    # delimiter for "tags" argument (default = space characters)
    my $delimiter = $args->{delimiter} || '';
    # case_sensitive (0/1, default = 1)
    my $case_sensitive = defined $args->{case_sensitive} ?
	$args->{case_sensitive} : 1;
    # sort_by (created_on, default = created_on)
    my $sort_by = $args->{sort_by} || 'created_on';
    # sort_order (ascend/descend, default = descend)
    my $sort_order = $args->{sort_order} || 'descend';
    # lastn (default = 0, no cutoff)
    my $lastn = $args->{lastn} || 0;

    my @tags = split_args($search, $delimiter, $case_sensitive)
	or return '';

    my $blog_id = $ctx->stash('blog_id');
    my $data = get_pd_indexes($blog_id) || get_db_indexes($blog_id)
	or return '';
    my %tindex = %{$data->{tindex}};
    my %eindex = %{$data->{eindex}};
    my %match;
    if ($case_sensitive) {
	foreach my $tag (@tags) {
	    foreach (@{$tindex{$tag}->{eids}}) {
		$match{$_} = exists $match{$_} ? $match{$_} + 1 : 1;
	    }
	}
    } else {
	foreach my $tag (@tags) {
	    foreach my $mtag (grep { lc $_ eq $tag } keys %tindex) {
		foreach (@{$tindex{$mtag}->{eids}}) {
		    $match{$_} = exists $match{$_} ? $match{$_} + 1 : 1;
		}
	    }
	}
    }
    my $count = scalar @tags;
    my @eids = grep { $match{$_} == $count } keys %match or return '';
    @eids = $sort_order eq 'descend' ?
	sort { $eindex{$b}->{created_on} <=> $eindex{$a}->{created_on} } @eids :
	sort { $eindex{$a}->{created_on} <=> $eindex{$b}->{created_on} } @eids;
    splice(@eids, $lastn) if $lastn && (scalar @eids > $lastn);
    require MT::Entry;
    my @entries;
    map { push @entries, MT::Entry->load($_) } @eids;

    my $res = '';
    my $tokens = $ctx->stash('tokens');
    my $builder = $ctx->stash('builder');
    my $i = 0;
    for my $e (@entries) {
	local $ctx->{__stash}{entry} = $e;
	local $ctx->{current_timestamp} = $e->created_on;
	local $ctx->{modification_timestamp} = $e->modified_on;
	my $out = $builder->build($ctx, $tokens, {
	    %$cond,
	    EntryIfExtended => $e->text_more ? 1 : 0,
	    EntryIfAllowComments => $e->allow_comments,
	    EntryIfCommentsOpen => $e->allow_comments && $e->allow_comments eq '1',
	    EntryIfAllowPings => $e->allow_pings,
	    EntriesHeader => !$i,
	    EntriesFooter => !defined $entries[$i+1]
	    });
	return $ctx->error($ctx->errstr) unless defined $out;
	$res .= $out;
	$i++;
    }
    $res;
}

sub most_related_entries {
    my ($ctx, $args, $cond) = @_;
    my $entry = $ctx->stash('entry')
	or return $ctx->_no_entry_error('MT' . $ctx->stash('tag'));
    return '' unless $entry->keywords;

    # case_sensitive (0/1, default = 1)
    my $case_sensitive = defined $args->{case_sensitive} ?
	$args->{case_sensitive} : 1;
    # sort_order (ascend/descend, default = descend)
    my $sort_order = $args->{sort_order} || 'descend';
    # lastn (default = 0, no cutoff)
    my $lastn = $args->{lastn} || 0;

    my @tags = split_tags($entry->keywords, $case_sensitive)
	or return '';

    my $blog_id = $ctx->stash('blog_id');
    my $data = get_pd_indexes($blog_id) || get_db_indexes($blog_id)
	or return '';
    my %tindex = %{$data->{tindex}};
    my %eindex = %{$data->{eindex}};
    my %match;
    if ($case_sensitive) {
	foreach my $tag (@tags) {
	    foreach (@{$tindex{$tag}->{eids}}) {
		next if $_ == $entry->id;
		$match{$_} = exists $match{$_} ? $match{$_} + 1 : 1;
	    }
	}
    } else {
	foreach my $tag (@tags) {
	    foreach my $mtag (grep { lc $_ eq $tag } keys %tindex) {
		foreach (@{$tindex{$mtag}->{eids}}) {
		    next if $_ == $entry->id;
		    $match{$_} = exists $match{$_} ? $match{$_} + 1 : 1;
		}
	    }
	}
    }
    my @eids = keys %match or return '';
    @eids = $sort_order eq 'descend' ?
	sort { $eindex{$b}->{created_on} <=> $eindex{$a}->{created_on} } @eids :
	sort { $eindex{$a}->{created_on} <=> $eindex{$b}->{created_on} } @eids;
    @eids = sort { $match{$b} <=> $match{$a} } @eids;
    splice(@eids, $lastn) if $lastn && (scalar @eids > $lastn);
    require MT::Entry;
    my @entries;
    map { push @entries, MT::Entry->load($_) } @eids;

    my $res = '';
    my $tokens = $ctx->stash('tokens');
    my $builder = $ctx->stash('builder');
    my $i = 0;
    for my $e (@entries) {
	local $ctx->{__stash}{entry} = $e;
	local $ctx->{current_timestamp} = $e->created_on;
	local $ctx->{modification_timestamp} = $e->modified_on;
	my $out = $builder->build($ctx, $tokens, {
	    %$cond,
	    EntryIfExtended => $e->text_more ? 1 : 0,
	    EntryIfAllowComments => $e->allow_comments,
	    EntryIfCommentsOpen => $e->allow_comments && $e->allow_comments eq '1',
	    EntryIfAllowPings => $e->allow_pings,
	    EntriesHeader => !$i,
	    EntriesFooter => !defined $entries[$i+1]
	    });
	return $ctx->error($ctx->errstr) unless defined $out;
	$res .= $out;
	$i++;
    }
    $res;
}

sub encode_urlplus {
    my $s = $_[0];
    return $s unless $_[1];
    require MT::Util;
    $s =~ tr/ /+/;
    MT::Util::encode_url($s);
}

eval {
    require MT::XSearch;
    MT::XSearch->add_search_plugin('Tagwire', {
	label => 'Tag(Keyword) Search',
	description => 'Tag(Keyword) Search plugin for MT-XSearch',
	on_execute => \&xsearch_on_execute,
	on_stash => \&xsearch_on_stash });
};

MT::Template::Context->add_container_tag('XSearchTags' => \&xsearch_tags);
sub xsearch_tags {
    my ($ctx, $args, $cond) = @_;
    my $r = MT::Request->instance;
    return '' unless defined $r->cache('Tagwire::xsearch_tags');
    my @tags = @{$r->cache('Tagwire::xsearch_tags')};
    my @res;
    my $builder = $ctx->stash('builder');
    my $tokens = $ctx->stash('tokens');
    foreach (@tags) {
	local $ctx->{__stash}{'Tagwire::tag'} = $_;
	defined(my $out = $builder->build($ctx, $tokens))
	    or return $ctx->error($ctx->errstr);
	push @res, $out;
    }
    my $glue = $args->{glue} || '';
    join $glue, @res;
}

sub xsearch_on_stash {
    $_[0]->stash('entry', $_[1]);
    $_[0]->{current_timestamp} = $_[1]->created_on;
    $_[0]->{modification_timestamp} = $_[1]->modified_on;
}

sub xsearch_on_execute {
    my $args = shift;
    my $blog_id = $args->{blog_id} or MT->error('Blog ID is required.');
    my $sort_by = $args->{sort_by} || 'created_on';
    my $sort_order = $args->{sort_order} || 'descend';
    my $delimiter = $args->{delimiter} || '';
    my $case_sensitive = defined $args->{case_sensitive} ?
	$args->{case_sensitive} : 1;

    my @tags = split_args($args->{search}, $delimiter, $case_sensitive)
	or return \();

    my $r = MT::Request->instance;
    $r->cache('Tagwire::xsearch_tags', \@tags);

    my $data = get_pd_indexes($blog_id) || get_db_indexes($blog_id)
	or return \();
    my %tindex = %{$data->{tindex}};
    my %eindex = %{$data->{eindex}};
    my %match;
    if ($case_sensitive) {
	foreach my $tag (@tags) {
	    foreach (@{$tindex{$tag}->{eids}}) {
		$match{$_} = exists $match{$_} ? $match{$_} + 1 : 1;
	    }
	}
    } else {
	foreach my $tag (@tags) {
	    foreach my $mtag (grep { lc $_ eq $tag } keys %tindex) {
		foreach (@{$tindex{$mtag}->{eids}}) {
		    $match{$_} = exists $match{$_} ? $match{$_} + 1 : 1;
		}
	    }
	}
    }
    my $count = scalar @tags;
    my @eids = grep { $match{$_} == $count } keys %match
	or return \();
    @eids = $sort_order eq 'descend' ?
	sort { $eindex{$b}->{created_on} <=> $eindex{$a}->{created_on} } @eids :
	sort { $eindex{$a}->{created_on} <=> $eindex{$b}->{created_on} } @eids;
    require MT::Entry;
    my @entries;
    map { push @entries, MT::Entry->load($_) } @eids;
    \@entries;
}

1;
