# ==================================================================
# Gossamer Links - enhanced directory management system
#
#   Website  : http://gossamer-threads.com/
#   Support  : http://gossamer-threads.com/scripts/support/
#   CVS Info : 087,071,086,086,085      
#   Revision : $Id: Build.pm,v 1.136 2008/06/25 19:23:48 brewt Exp $
#
# Copyright (c) 2001 Gossamer Threads Inc.  All Rights Reserved.
# Redistribution in part or in whole strictly prohibited. Please
# see LICENSE file for full details.
# ==================================================================

package Links::Build;
# ==================================================================
use strict;
use Links qw/:objects :payment/;
use Links::SiteHTML;
use GT::AutoLoader;
use vars qw/$GRAND_TOTAL/;

sub build {
# -----------------------------------------------------------------
# Returns a specified template parsed.
#
    my $build = shift;
    my $code = exists $Links::Build::{"build_$build"} ? *{$Links::Build::{"build_$build"}}{CODE} : _compile("build_$build");
    defined $code or die "Invalid method: build_$build called.";

    $PLG->dispatch("build_$build", $code, @_);
}

$COMPILE{build_home} = __LINE__ . <<'END_OF_SUB';
sub build_home {
# ------------------------------------------------------------------
# Generate the home page.
#
    $GRAND_TOTAL ||= _grand_total();

    my $category = $DB->table('Category');
    $category->select_options("ORDER BY $CFG->{build_category_sort}") if $CFG->{build_category_sort};
    my $sth = $category->select({ FatherID => 0 });

    my $root = [];
    while (my $cat = $sth->fetchrow_hashref) {
        $cat->{URL} = "$CFG->{build_root_url}/" . $category->as_url($cat->{Full_Name}) . "/" . ($CFG->{build_index_include} ? $CFG->{build_index} : '');
        push @$root, $cat;
    }

    # Plugins can use the build_category_loop hook to change the category
    # results before they are returned to the template.
    $PLG->dispatch(build_category_loop => sub { } => $root);

    my $print_cat;
    my $cat_list = sub {
        return $print_cat if defined $print_cat;
        return $print_cat = Links::SiteHTML::display('print_cat', [{}, @$root])
    };

    return Links::SiteHTML::display('home', { category => $cat_list, category_loop => $root, grand_total => $GRAND_TOTAL });
}
END_OF_SUB

$COMPILE{build_new} = __LINE__ . <<'END_OF_SUB';
sub build_new {
# ------------------------------------------------------------------
# Generate the what's new page. Takes as options:
#
#       mh => new links per page
#       nh => page number
#       sb => field to sort by
#       so => ascending or descending order
#       gb => 1/0 (group by category name).
#
    my $opts = shift;
    if (ref $opts ne 'HASH') {
        Links::debug("Invalid option passed to build_new: $opts") if $Links::DEBUG;
        return;
    }
    if (! exists $opts->{sb}) {
        my $sort = $CFG->{build_sort_order_new} || '';
        $opts->{sb} = $sort;
        $opts->{so} = '';
    }
    $opts->{mh} = exists $opts->{mh} ? $opts->{mh} : 1000;
    $opts->{nh} = exists $opts->{nh} ? $opts->{nh} : 1;
    $opts->{sb} = exists $opts->{sb} ? $opts->{sb} : 'Add_Date';
    $opts->{gb} = exists $opts->{gb} ? $opts->{gb} : $CFG->{build_new_gb};

    my $cat_db  = $DB->table('Category');
    my $link_db = $DB->table('Links');
    $GRAND_TOTAL ||= _grand_total();

    my %filter = (
        isValidated => 'Yes',
        isNew => 'Yes',
        mh    => $opts->{mh},
        nh    => $opts->{nh},
        sb    => $opts->{sb},
        so    => $opts->{so},
        ww    => 1
    );

# Should not select unpaid and expired links if payment is enabled
    $filter{ExpiryDate} = '>=' . time if $CFG->{payment}->{enabled};

    my $sth      = $link_db->query_sth(\%filter);
    my $total    = $link_db->hits;
    my $cur_date = '';
    Links::init_date();

    my (@date_order, %grouped_output);

# Get the links.
    my $results = $sth->fetchall_hashref;
    $link_db->add_reviews($results);

# Get the category names.
    my $catlink = $DB->table('CatLinks','Category');
    my %names   = $catlink->select('LinkID', 'Full_Name', { LinkID => [map $_->{ID}, @$results] })->fetchall_list;

    for my $link (@$results) {
        my $date = $link->{Add_Date};
        my $long_date = GT::Date::date_transform($link->{Add_Date}, GT::Date::FORMAT_DATE, $CFG->{date_long_format});
        $date ne $cur_date and push @date_order, $long_date;
        if ($opts->{gb}) {
            push @{$grouped_output{$long_date}->{$names{$link->{ID}}}}, Links::SiteHTML::tags('link', $link);
        }
        else {
            push @{$grouped_output{$long_date}->{'none'}}, Links::SiteHTML::tags('link', $link);

        }
        $cur_date = $date;
    }
    my @dates;
    foreach my $date (@date_order) {
        my @links;
        if ($opts->{gb}) {
            foreach my $cat (sort keys %{$grouped_output{$date}}) {
                $grouped_output{$date}->{$cat}->[0]->{title_linked} = sub { build('title_linked', { name => $cat, complete => 1 }) };
                $grouped_output{$date}->{$cat}->[0]->{title_loop} = build('title', $cat);
                push @links, @{$grouped_output{$date}->{$cat}};
            }
        }
        else {
            push @links, @{$grouped_output{$date}->{'none'}};
        }
        push @dates, { new_date => $date, links => \@links };
    }

    my $ret;
    my $output = sub {
        return $ret if defined $ret;
        $ret = '';
        for my $date (@date_order) {
            $ret .= "</blockquote></p>" if $ret;
            $ret .= "<p><b>$date</b><blockquote>";
            my @links;
            if ($opts->{gb}) {
                for my $cat (sort keys %{$grouped_output{$date}}) {
                    my $title = build('title_linked', { name => $cat, complete => 1 });
                    $ret .= $title;
                    $ret .= join("", map { Links::SiteHTML::display('link', $_) } @{$grouped_output{$date}->{$cat}});
                }
            }
            else {
                $ret .= join("", map { Links::SiteHTML::display('link', $_) } @{$grouped_output{$date}->{'none'}});
            }
        }
        $ret .= "</blockquote></p>";
        return $ret;
    };

    my $new = Links::language('LINKS_NEW');
    return Links::SiteHTML::display(new => {
        total             => $total,
        grand_total       => $GRAND_TOTAL,
        link_results      => $output,
        link_results_loop => $results,
        main_title_loop   => build('title', $new),
        title             => sub { build('title_unlinked', $new) },
        main_title_linked => sub { build('title_linked', $new) },
        title_linked_loop => \@dates
    });
}
END_OF_SUB

$COMPILE{build_new_index} = __LINE__ . <<'END_OF_SUB';
sub build_new_index {
# ------------------------------------------------------------------
# Build a what's new index page grouped by long date.
#
    Links::init_date();

    require GT::SQL::Condition;
    my $cond = GT::SQL::Condition->new(
        isNew => '=' => 'Yes',
        VIEWABLE
    );

    my $link_db  = $DB->table('Links');
    $link_db->select_options("GROUP BY Add_Date", "ORDER BY Add_Date DESC");
    my $sth      = $link_db->select(Add_Date => 'COUNT(*)' => $cond);
    my $total    = 0;
    $GRAND_TOTAL ||= _grand_total();

    my $results = '';
    my @loop;
    while (my ($date, $count) = $sth->fetchrow_array) {
        my $long_date = GT::Date::date_transform($date, GT::Date::FORMAT_DATE, $CFG->{date_long_format});
        $date =~ s/\s(.*)//;
        $results .= qq|<li><a href="$CFG->{build_new_url}/$date$CFG->{build_extension}">$long_date</a> ($count)</li>|;
        $total += $count;
        push @loop, { date => $long_date, url => "$CFG->{build_new_url}/$date$CFG->{build_extension}", count => $count };
    }

    my $new = Links::language('LINKS_NEW');
    return Links::SiteHTML::display(new => {
        total             => $total,
        grand_total       => $GRAND_TOTAL,
        link_results      => $results,
        link_results_loop => \@loop,
        main_title_loop   => build('title', $new),
        title             => sub { build(title_unlinked => $new) },
        main_title_linked => sub { build(title_linked => $new) },
        new_index         => 1
    });
}
END_OF_SUB

$COMPILE{build_new_subpage} = __LINE__ . <<'END_OF_SUB';
sub build_new_subpage {
# -----------------------------------------------------------------------------
# Generate a what's new sub page. Takes a hash reference containing a date key
# and value.
#
    Links::init_date();

    my $opts = shift;
    if (ref $opts ne 'HASH') {
        Links::debug("Invalid argument to build_new_subpage: $opts") if $Links::DEBUG;
        return;
    }
    my $date = $opts->{date};
    if (!GT::Date::date_is_valid($date)) {
        Links::debug("Invalid date passed to build_new_subpage: $date") if $Links::DEBUG;
        return;
    }
    my $link_db = $DB->table('Links');
    my $cat_db  = $DB->table('Category');
    my $mh      = $opts->{mh};
    my $nh      = $opts->{nh};
    $link_db->select_options("ORDER BY $CFG->{build_sort_order_new}") if $CFG->{build_sort_order_new};
    if ($mh and $nh) {
        my $offset = ($nh-1) * $mh;
        $link_db->select_options("LIMIT $offset, $mh");
    }

    my $sth         = $link_db->select({ isNew => 'Yes', Add_Date => $date }, VIEWABLE);
    my $new_total   = $link_db->hits;
    my $long_date   = GT::Date::date_transform($date, GT::Date::FORMAT_DATE, $CFG->{date_long_format});
    my $total       = $link_db->hits;
    my $gb          = $CFG->{build_new_gb};
    $GRAND_TOTAL  ||= _grand_total();
    my $output      = {};
    my $out         = '';

# Get the links.
    my $results = $sth->fetchall_hashref;
    $link_db->add_reviews($results);
    my @link_results_loop;
    @link_results_loop = map Links::SiteHTML::tags('link', $_) => @$results unless $gb;

# Get the category names.
    my $catlink = $DB->table('CatLinks','Category');
    my %names = $catlink->select('LinkID', 'Full_Name', { LinkID => [map $_->{ID}, @$results] })->fetchall_list;
    if ($gb) {
        for my $link (@$results) {
            push @{$output->{$names{$link->{ID}}}}, Links::SiteHTML::tags('link', $link);
        }
    }
    my ($toolbar, %paging);
    if ($mh and ($total > $mh)) {
        my $first_url = $CFG->{build_new_url} . "/" . $date . $CFG->{build_extension};
        my $next_url  = $CFG->{build_new_url} . "/" . $date . "_";
        $toolbar = build('toolbar', { first_url => $first_url, next_url => $next_url, numlinks => $new_total, nh => $nh, mh => $mh });
        my ($new_url) = $CFG->{build_new_url} =~ m|^\Q$CFG->{build_root_url}\E/(.+)|;
        %paging = (
            page         => "$new_url/$date",
            page_format  => 2,
            num_hits     => $new_total,
            max_hits     => $mh,
            current_page => $nh
        );
    }
    if ($gb) {
        foreach my $name (sort { lc $a cmp lc $b } keys %$output) {
            $output->{$name}->[0]->{title_linked} = sub { build('title_linked', { name => $name, complete => 1 }) };
            $output->{$name}->[0]->{title_loop} = build('title', $name);
            push @link_results_loop, @{$output->{$name}};
        }
    }
    my $links;
    $out = sub {
        return $links if defined $links;
        $gb or return $links = join "", map { Links::SiteHTML::display(link => $_) } @$results;
        for my $name (sort { lc $a cmp lc $b } keys %$output) {
            my $title = build(title_linked => { name => $name, complete => 1 });
            $links .= "<p>$title" . join "", map { Links::SiteHTML::display(link => $_) } @{$output->{$name}};
        }
        return $links;
    };

    my $new = Links::language('LINKS_NEW');
    return Links::SiteHTML::display(new => {
        total             => $total,
        grand_total       => $GRAND_TOTAL,
        link_results      => $out,
        link_results_loop => \@link_results_loop,
        main_title_loop   => build('title', "$new/$long_date"),
        title             => sub { build('title_unlinked', "$new/$long_date") },
        main_title_linked => sub { build('title_linked', "$new/$long_date") },
        next_span         => $toolbar,
        paging            => \%paging
    });
}
END_OF_SUB

$COMPILE{build_cool} = __LINE__ . <<'END_OF_SUB';
sub build_cool {
# ------------------------------------------------------------------
# Generate the what's cool page. Takes as options:
#
#       mh => new links per page
#       nh => page number
#       sb => field to sort by
#       so => ascending or descending order
#       gb => 1/0 (group by category name).
#
    my $opts = shift;
    if (ref $opts ne 'HASH') {
        Links::debug("Invalid option passed to build_cool: $opts") if $Links::DEBUG;
        return;
    }
    if (! exists $opts->{sb}) {
        $opts->{sb} = $CFG->{build_sort_order_cool} || '';
        $opts->{so} = '';
    }
    $opts->{mh} ||= $CFG->{build_links_per_page} || 25;
    $opts->{nh} ||= 1;
    $opts->{gb} ||= $CFG->{build_cool_gb};

    my $cat_db  = $DB->table('Category');
    my $link_db = $DB->table('Links');
    $GRAND_TOTAL ||= _grand_total();

    my %filter = (
        isPopular   => 'Yes',
        isValidated => 'Yes',
        mh => $opts->{mh},
        nh => $opts->{nh},
        sb => $opts->{sb},
        so => $opts->{so},
        ww => 1
    );

# Should not select unpaid and expired links if payment is enabled
    $filter{ExpiryDate} = '>=' . time if $CFG->{payment}->{enabled};

    my $sth       = $link_db->query_sth(%filter);
    my $total     = $link_db->hits;
    my $sub_total = 0;
    my $output    = '';
    my %grouped_output;

# Get the links.
    my $results = $sth->fetchall_hashref;
    $link_db->add_reviews($results);
    my @link_results_loop;
    @link_results_loop = map Links::SiteHTML::tags('link', $_) => @$results unless $opts->{gb};

# Get the category names.
    my $catlink = $DB->table('CatLinks','Category');
    my %names = $catlink->select(LinkID => Full_Name => { LinkID => [map $_->{ID}, @$results] })->fetchall_list;
    for my $link (@$results) {
        $opts->{gb} and push @{$grouped_output{$names{$link->{ID}}}}, Links::SiteHTML::tags('link', $link);
        $sub_total++;
    }
    if ($opts->{gb}) {
        foreach my $cat (sort { lc $a cmp lc $b } keys %grouped_output) {
            $grouped_output{$cat}->[0]->{title_linked} = sub { build(title_linked => { name => $cat, complete => 1 }) };
            $grouped_output{$cat}->[0]->{title_loop} = build('title', $cat);
            push @link_results_loop, @{$grouped_output{$cat}};
        }
    }
    my $links;
    $output = sub {
        return $links if defined $links;
        $opts->{gb} or return $links = join '', map { Links::SiteHTML::display('link', $_) } @$results;
        $links = '';
        foreach my $cat (sort { lc $a cmp lc $b } keys %grouped_output) {
            my $title = build('title_linked', { name => $cat, complete => 1 });
            $links .= "<p>$title" . join '', map { Links::SiteHTML::display(link => $_) } @{$grouped_output{$cat}};
        }
        return $links;
    };
    my $percent = ($CFG->{build_pop_cutoff} < 1) ? $CFG->{build_pop_cutoff} * 100 . "%" : $CFG->{build_pop_cutoff};

    my ($toolbar, %paging);
    if ($total > $opts->{mh}) {
        $toolbar = build(toolbar => { url => "$CFG->{build_cool_url}/", numlinks => $total, nh => $opts->{nh}, mh => $opts->{mh} });
        my ($cool_url) = $CFG->{build_cool_url} =~ m|^\Q$CFG->{build_root_url}\E/(.+)|;
        %paging = (
            page         => "$cool_url/",
            page_format  => 1,
            num_hits     => $total,
            max_hits     => $opts->{mh},
            current_page => $opts->{nh}
        );
    }

    my $cool = Links::language('LINKS_COOL');
    return Links::SiteHTML::display(cool => {
        total             => $total,
        grand_total       => $GRAND_TOTAL,
        link_results      => $output,
        main_title_loop   => build('title', $cool),
        title             => sub { build(title_unlinked => $cool) },
        main_title_linked => sub { build(title_linked => $cool) },
        percent           => $percent,
        next_span         => $toolbar,
        paging            => \%paging,
        link_results_loop => \@link_results_loop
    });
}
END_OF_SUB

$COMPILE{build_rating} = __LINE__ . <<'END_OF_SUB';
sub build_rating {
# ------------------------------------------------------------------
# Generate the rating pages.
#
    my $links = $DB->table('Links');

    require GT::SQL::Condition;
    my $cond = GT::SQL::Condition->new('Votes', '>=', 10, VIEWABLE);
    $GRAND_TOTAL ||= _grand_total();

    $links->select_options("ORDER BY Rating DESC", "LIMIT 10");
    my $rated = $links->select($cond);

    $links->select_options("ORDER BY Votes DESC", "LIMIT 10");
    my $voted = $links->select($cond);

# Now build the html.
    my ($top_rated, $top_votes) = ('', '');

    my (@top_votes_loop, @top_rated_loop);
    while (my $link = $voted->fetchrow_hashref) {
        $link->{detailed_url} = $CFG->{build_detail_url} . '/' . $links->detailed_url($link->{ID}) if $CFG->{build_detailed};
        push @top_votes_loop, $link;
        $top_votes .= qq~<tr><td align=center>$link->{Rating}</td><td align=center>$link->{Votes}</td><td><a href="$link->{URL}">$link->{Title}</a></td></tr>\n~;
    }
    while (my $link = $rated->fetchrow_hashref) {
        $link->{detailed_url} = $CFG->{build_detail_url} . '/' . $links->detailed_url($link->{ID}) if $CFG->{build_detailed};
        push @top_rated_loop, $link;
        $top_rated .= qq~<tr><td align=center>$link->{Rating}</td><td align=center>$link->{Votes}</td><td><a href="$link->{URL}">$link->{Title}</a></td></tr>\n~;
    }

# And write it to a file.
    return Links::SiteHTML::display(rate_top => {
        top_rated       => $top_rated,
        top_rated_loop  => \@top_rated_loop,
        top_votes_loop  => \@top_votes_loop,
        top_votes       => $top_votes,
        main_title_loop => build('title', Links::language('LINKS_TOPRATED'), $CFG->{build_ratings_url})
    });
}
END_OF_SUB

$COMPILE{build_detailed} = __LINE__ . <<'END_OF_SUB';
sub build_detailed {
# ------------------------------------------------------------------
# Builds a single detailed page, takes either a link hash ref, or
# a link id.
#
    if (! $CFG->{build_detailed}) {
        return Links::SiteHTML::display(error => { error => Links::language('BUILD_DETAILED_DISABLED') });
    }

    my $link_db    = $DB->table('Links');
    $GRAND_TOTAL ||= _grand_total();

    my $link = shift;
    if (ref $link ne 'HASH') {
        return Links::SiteHTML::display(error => { error => Links::language('BUILD_DETAILED_ARGS', $link) });
    }
    $link->{ID} ||= $IN->param('ID');
    if (exists $link->{ID}) {
        my $id = $link->{ID};
        my $link_row = $link_db->get($id, 'HASH');
        @$link{keys %$link_row} = values %$link_row;
        $link_db->add_reviews([ $link ]);
        if (! $link) {
            return Links::SiteHTML::display(error => { error => Links::language('BUILD_DETAILED_INVALIDID', $id) });
        }
        elsif ($link->{isValidated} ne 'Yes') {
            return Links::SiteHTML::display(error => { error => Links::language('BUILD_DETAILED_UNVAL') });
        }
        elsif ($CFG->{payment}->{enabled} and $link->{ExpiryDate} < time) {
            return Links::SiteHTML::display(error => { error => Links::language('BUILD_DETAILED_EXPIRED') });
        }
    }

    my ($cat_id, $cat_name);
    if ($link->{CategoryID} and $link->{Full_Name}) {
        $cat_id = $link->{CategoryID};
        $cat_name = $link->{Full_Name};
    }
    else {
        ($cat_id, $cat_name) = each %{$link_db->get_categories($link->{ID})};
    }

# Figure out the next/prev links.
    my $catlnk_db   = $DB->table('Links', 'CatLinks');
    $catlnk_db->select_options("ORDER BY $CFG->{build_sort_order_category}") if $CFG->{build_sort_order_category};
    my $sth = $catlnk_db->select('Links.ID' => { CategoryID => $cat_id }, VIEWABLE);
    my ($next, $prev);
    while (my $id = $sth->fetchrow) {
        if ($id == $link->{ID}) {
            $next = $sth->fetchrow;
            last;
        }
        else {
            $prev = $id;
        }
    }

    my ($next_url, $prev_url) = $DB->table('Links')->category_detailed_url($cat_id, $next, $prev);

    $link->{Category_Template} = $DB->table('Category')->template_set($cat_id);

# build_title generates the title loop by using / as a category delimiter.
# Since the link title can have /'s in it, we'll push that entry onto the loop
# ourselves.  Note that the deprecated title and title_linked variables are
# still broken.
    my $title_loop = build('title', $cat_name);
    push @$title_loop, { Name => $link->{Title}, URL => $CFG->{build_detail_url} . '/' . $link_db->detailed_url($link->{ID}) };

    return Links::SiteHTML::display('detailed', {
        %$link,
        title_loop     => $title_loop,
        title          => sub { build('title_unlinked', "$cat_name/$link->{Title}") },
        title_linked   => sub { build('title_linked', "$cat_name/$link->{Title}") },
        grand_total    => $GRAND_TOTAL,
        next           => $next,
        prev           => $prev,
        next_url       => $next_url ? "$CFG->{build_detail_url}/$next_url" : '',
        prev_url       => $prev_url ? "$CFG->{build_detail_url}/$prev_url" : ''
    });
}
END_OF_SUB

$COMPILE{build_category} = __LINE__ . <<'END_OF_SUB';
sub build_category {
# ------------------------------------------------------------------
# Build a single category page. Takes as argument a category hash or id
# number and an options hash.
#
    my $cat_db     = $DB->table('Category');
    my $link_db    = $DB->table('Links');
    my $catlink_db = $DB->table('Links', 'CatLinks');
    my $related_db = $DB->table('CatRelations');
    $GRAND_TOTAL ||= _grand_total();

    my $opts       = shift;
    if (ref $opts ne 'HASH') {
        Links::debug("Invalid argument passed to build_category: $opts") if $Links::DEBUG;
        return;
    }

# Load our category info.
    my $category;
    $opts->{id} ||= $IN->param('ID');
    if ($opts->{id}) {
        $category = $cat_db->get($opts->{id}, 'HASH');
        if (! $category) {
            Links::debug("Invalid category id passed to build_category: $opts->{id}") if $Links::DEBUG;
            return;
        }
    }

# Get our options.
    $opts->{mh} = exists $opts->{mh} ? $opts->{mh} : $CFG->{build_span_pages} ? $CFG->{build_links_per_page} : 5000;
    $opts->{nh} = exists $opts->{nh} ? $opts->{nh} : 1;
    $opts->{sb} = exists $opts->{sb} ? $opts->{sb} : $CFG->{build_sort_order_category};
    $opts->{so} = exists $opts->{so} ? $opts->{so} : '';
    if ($opts->{sb} =~ /\b(?:asc|desc)\b/i) {
        $opts->{so} = '';
    }
    $opts->{cat_sb} = exists $opts->{cat_sb} ? $opts->{cat_sb} : $CFG->{build_category_sort};
    $opts->{cat_so} = exists $opts->{cat_so} ? $opts->{cat_so} : '';
    if ($opts->{cat_sb} =~ /\b(?:asc|desc)\b/i) {
        $opts->{cat_so} = '';
    }

# Figure out the template set to use.
    $category->{Category_Template} ||= $cat_db->template_set($category->{ID});

# Get our output vars.
    my %tplvars = (
        %$category,
        category_id   => $category->{ID},
        category_name => $category->{Full_Name},
        header        => $category->{Header},
        footer        => $category->{Footer},
        meta_name     => $category->{Meta_Description},
        meta_keywords => $category->{Meta_Keywords},
        description   => $category->{Description},
        random        => int rand 10000,
        random1       => int rand 10000,
        random2       => int rand 10000,
        random3       => int rand 10000
    );

# Clean up the name.
    my $clean_name = $cat_db->as_url($category->{Full_Name});
    my $build_title = $category->{Full_Name};
    $build_title .= '/' . Links::language('LINKS_PAGE', $opts->{nh}) if $opts->{nh} and $opts->{nh} > 1;

    $tplvars{title_loop}   = build('title', $build_title);
    $tplvars{title_linked} = sub { build('title_linked', $build_title) };
    $tplvars{title}        = sub { build('title_unlinked', $build_title) };

    $tplvars{category_name_escaped} = GT::CGI->escape($category->{Full_Name});
    $tplvars{category_clean}   = $tplvars{title};
    ($tplvars{category_short}) = $tplvars{category_name} =~ m|([^/]+)$|;

# Prepare the condition; don't add the ExpiryDate handling - it gets added later
    my $cond = GT::SQL::Condition->new(
        CategoryID  => '=' => $category->{ID},
        isValidated => '=' => 'Yes'
    );

# "Optional" payment categories are a hassle, as we have to do two selects,
# then balance out the mh/nh variables between the two.
    my ($optional_sth, $sth);
    my @select_options;
    push @select_options, "ORDER BY $opts->{sb} $opts->{so}" if $opts->{sb};

# Load payment info if payment is enabled. Change sort order by paid links
# first then free links if payment for this category is optional. If payment
# is required, we need to remove unpaid links
    if ($CFG->{payment}->{enabled}) {
        require Links::Payment;
        my $payment_info = Links::Payment::cat_payment_info($opts->{id});

        if ($payment_info->{mode} == OPTIONAL and $CFG->{build_sort_paid_first}) {
            my $paycond = GT::SQL::Condition->new($cond);
            $paycond->add(ExpiryDate => '>=' => time, ExpiryDate => '<=' => UNLIMITED);

            my $offset = ($opts->{nh} - 1) * $opts->{mh};
            $catlink_db->select_options(@select_options);
            $catlink_db->select_options("LIMIT $opts->{mh} OFFSET $offset");
            $optional_sth = $catlink_db->select('Links.*', $paycond);

            $cond->add(ExpiryDate => '=' => FREE);
        }
        else {
            # 1) This is an else (instead of elsif ($payment_info->{mode} == REQUIRED)) because the
            # run-time count updating code cannot efficiently take category settings into account
            # as doing so requires either subselects (which older MySQL doesn't support), or a fair
            # bit of Perl code; a single fast count to determine whether the check is necessary
            # won't work.  The end result is that counts would be off.
            # 2) Even if this was an elsif, we can't include ExpiryDate <= UNLIMITED (to exclude
            # free links) because links being free is the default for imported, upgraded, and
            # admin-added links, which we don't want to exclude from REQUIRED categories.
            $cond->add(ExpiryDate => '>=' => time);
        }
    }

    my @results;
    my ($paid_hits, $paid_rows, $offset, $max_hits) = (0, 0, ($opts->{nh} - 1) * $opts->{mh}, $opts->{mh});
    if ($optional_sth) {
        push @results, @{$optional_sth->fetchall_hashref};
        $paid_rows = $optional_sth->rows;
        $paid_hits = $catlink_db->hits;
        if ($paid_rows == $opts->{mh}) {
            $offset = $max_hits = 0;
        }
        elsif ($paid_rows > 0) {
            $offset = 0;
            $max_hits = $opts->{mh} - $paid_rows;
        }
        else {
            $offset -= $paid_hits;
        }
    }
    my $hits;
# Select links from required categories, not-accepted categories, and optional
# categories whose paid hits haven't filled the page
    if ($max_hits) { # $max_hits will be 0 when mh paid links are already listed
        $catlink_db->select_options(@select_options);
        $catlink_db->select_options("LIMIT $max_hits OFFSET $offset");
        my $sth = $catlink_db->select('Links.*' => $cond);
        push @results, @{$sth->fetchall_hashref};
        $hits = $catlink_db->hits;
    }
    else {
        $hits = $catlink_db->count($cond);
    }

    my $numlinks = $tplvars{total} = $hits + $paid_hits;
    $tplvars{total_optional_paid} = $paid_hits;

# Get the links.
    $link_db->add_reviews(\@results);
    my @links_loop = map Links::SiteHTML::tags('link', $_, $category->{ID}) => @results;
    $tplvars{links_loop} = \@links_loop;
    $tplvars{links_count} = @links_loop;
    my $links;
    $tplvars{links} = sub {
        return $links if defined $links;
        $links = '';
        for my $link (@results) {
            $link->{Category_Template} = $category->{Category_Template} if $category->{Category_Template};
            $links .= Links::SiteHTML::display('link', $link);
        }
        return $links;
    };

# Get the subcategories and related categories as either Yahoo style (integrated) or
# separated into two outputs..
    my @cat_loop;
    $tplvars{category_loop} = \@cat_loop;
    if ($CFG->{build_category_yahoo}) {
        my @subcat_ids = $cat_db->select(ID => { FatherID => $category->{ID} })->fetchall_list;
        my %related_ids = $related_db->select(qw/RelatedID RelationName/ => { CategoryID => $category->{ID} })->fetchall_list;
        if (@subcat_ids or keys %related_ids) {
            $cat_db->select_options("ORDER BY $opts->{cat_sb} $opts->{cat_so}") if $opts->{cat_sb};
            my $sth = $cat_db->select({ ID => [@subcat_ids, keys %related_ids] });
            my @rel_loop;
            while (my $cat = $sth->fetchrow_hashref) {
                $cat->{URL} = "$CFG->{build_root_url}/" . $cat_db->as_url($cat->{Full_Name}) . "/" . ($CFG->{build_index_include} ? $CFG->{build_index} : '');
                $cat->{RelationName} = '';
                if (exists $related_ids{$cat->{ID}}) {
                    $cat->{Related} = 1;
                    $cat->{RelationName} = $related_ids{$cat->{ID}};
# Relations with a custom name need to be re-sorted
                    if ($cat->{RelationName}) {
                        push @rel_loop, $cat;
                        next;
                    }
                }
                push @cat_loop, $cat;
            }
# Re-sort related categories using their RelationName rather than the related
# category's name
            RELATION: while (my $cat = pop @rel_loop) {
                for (my $i = 0; $i < @cat_loop; $i++) {
                    my $name = $cat_loop[$i]->{RelationName} ? $cat_loop[$i]->{RelationName} : $cat_loop[$i]->{Name};
                    if (lc $cat->{RelationName} lt lc $name) {
                        splice @cat_loop, $i, 0, $cat;
                        next RELATION;
                    }
                }
                push @cat_loop, $cat;
            }
            my $print_cat;
            $tplvars{category} = sub {
                return $print_cat if defined $print_cat;
                return $print_cat = Links::SiteHTML::display('print_cat', [$category, @cat_loop]);
            };
        }
        else {
            $tplvars{category} = '';
        }
    }
    else {
# Separate the output.
        $cat_db->select_options("ORDER BY $opts->{cat_sb} $opts->{cat_so}") if $opts->{cat_sb};
        $sth = $cat_db->select({ FatherID => $category->{ID} });
        while (my $cat = $sth->fetchrow_hashref) {
            $cat->{URL} = "$CFG->{build_root_url}/" . $cat_db->as_url($cat->{Full_Name}) . "/" . ($CFG->{build_index_include} ? $CFG->{build_index} : '');
            push @cat_loop, $cat;
        }
        if (@cat_loop) {
            my $print_cat;
            $tplvars{category} = sub {
                return $print_cat if defined $print_cat;
                return $print_cat = Links::SiteHTML::display('print_cat', [$category, @cat_loop]);
            };
        }
        else {
            $tplvars{category} = '';
        }
        $tplvars{related}  = '';
        $tplvars{related_loop} = [];

        my %related_ids = $related_db->select(qw/RelatedID RelationName/ => { CategoryID => $category->{ID} })->fetchall_list;
        if (keys %related_ids) {
            $cat_db->select_options("ORDER BY $opts->{cat_sb} $opts->{cat_so}") if $opts->{cat_sb};
            my $sth = $cat_db->select({ ID => [keys %related_ids] });
            while (my $cat = $sth->fetchrow_hashref) {
                my $url = $CFG->{build_root_url} . "/" . $cat_db->as_url($cat->{Full_Name}) . "/" . ($CFG->{build_index_include} ? $CFG->{build_index} : '');
                $cat->{URL} = $url;
                $cat->{RelationName} = $related_ids{$cat->{ID}};
                push @{$tplvars{related_loop}}, $cat;
                $tplvars{related} .= qq|<li><a href="$url">| . ($related_ids{$cat->{ID}} || $cat->{Full_Name}) . "</a></li>";
            }
        }
    }

    # Plugins can use the build_category_loop hook to change the category
    # results before they are returned to the template.
    $PLG->dispatch(build_category_loop => sub { } => \@cat_loop);

# Get the header and footer from file if it exists, otherwise assume it is html.
    if ($tplvars{header} and $tplvars{header} =~ /^\S{1,20}$/ and -e "$CFG->{admin_root_path}/headers/$tplvars{header}") {
        local (@ARGV, $/) = "$CFG->{admin_root_path}/headers/$tplvars{header}";
        $tplvars{header} = <>;
    }
    if ($tplvars{footer} and $tplvars{footer} =~ /^\S{1,20}$/ and -e "$CFG->{admin_root_path}/footers/$tplvars{footer}") {
        local (@ARGV, $/) = "$CFG->{admin_root_path}/footers/$tplvars{footer}";
        $tplvars{footer} = <>;
    }

# If we are spanning pages, figure out toolbars and such.
    if ($CFG->{build_span_pages}) {
        my $lpp = $CFG->{build_links_per_page};
        my $nh  = $opts->{nh};
        my $url = $CFG->{build_root_url}  . "/" . $clean_name;
        $tplvars{next} = $tplvars{prev} = "";
        if ($numlinks > ($nh * $lpp)) {
            $tplvars{next} = "$url/$CFG->{build_more}" . ($nh + 1) . "$CFG->{build_extension}";
        }
        if ($nh == 2) {
            $tplvars{prev} = "$url/" . ($CFG->{build_index_include} ? $CFG->{build_index} : '');
        }
        elsif ($nh > 2) {
            $tplvars{prev} = "$url/$CFG->{build_more}" . ($nh - 1) . "$CFG->{build_extension}";
        }
        if ($tplvars{next} or $tplvars{prev}) {
            $tplvars{next_span} = build('toolbar', { url => $url, numlinks => $numlinks, nh => $nh });
            $tplvars{paging} = {
                page         => "$clean_name/",
                page_format  => 1,
                num_hits     => $numlinks,
                max_hits     => $opts->{mh},
                current_page => $opts->{nh}
            };
        }
    }

    return Links::SiteHTML::display('category', \%tplvars);
}
END_OF_SUB

$COMPILE{build_reset_hits} = __LINE__ . <<'END_OF_SUB';
sub build_reset_hits {
# ------------------------------------------------------------------
# Remove old hit and rate tracking information.
#
    Links::init_date();
    my $delete_by = GT::Date::date_get(time - 2*24*60*60, '%yyyy%-%mm%-%dd%');
    my $click = $DB->table('ClickTrack');
    $click->delete(GT::SQL::Condition->new('Created', '<', $delete_by));
}
END_OF_SUB

$COMPILE{build_orphan_check} = __LINE__ . <<'END_OF_SUB';
sub build_orphan_check {
# ------------------------------------------------------------------
# Checks for orphaned links and returns a list of ids.
#
    my $opts = shift;
    my $sel  = $opts->{select} || 'ID';
    my $rel  = $DB->table('Links', 'CatLinks');
    my $sth  = $rel->select('left_join', $sel, { LinkID => undef });
    my @output;
    if ($opts->{select}) {
        while (my $link = $sth->fetchrow_hashref) {
            push @output, $link;
        }
    }
    else {
        while (my $id = $sth->fetchrow_array) {
            push @output, $id;
        }
    }
    return @output;
}
END_OF_SUB

$COMPILE{build_catlinks_orphan_check} = __LINE__ . <<'END_OF_SUB';
sub build_catlinks_orphan_check {
# ------------------------------------------------------------------
# Checks for orphaned CatLinks and returns a list of CategoryID/LinkIDs.
#
    return (@{$DB->table('CatLinks', 'Links')->select('left_join', 'CategoryID', 'LinkID', { ID => undef })->fetchall_hashref},
        @{$DB->table('CatLinks', 'Category')->select('left_join', 'CategoryID', 'LinkID', { ID => undef })->fetchall_hashref});
}
END_OF_SUB

$COMPILE{build_new_flags} = __LINE__ . <<'END_OF_SUB';
sub build_new_flags {
# ------------------------------------------------------------------
# Update the isNew flag based on Add_Date.
#
    Links::init_date();
    my $opts = shift || {};
    my $date = GT::Date::date_sub(GT::Date::date_get(), $CFG->{build_new_cutoff});
    my $links = $DB->table('Links');
    $links->indexing(0);
    my $cond = GT::SQL::Condition->new(
        isNew => '=' => 'Yes',
        Add_Date => '<' => $date,
        VIEWABLE
    );
# Unset isNew for links which aren't new anymore
    my $old_rows = $links->count($cond);
    if ($old_rows) {
        $links->update({ isNew => 'No' }, $cond, { GT_SQL_SKIP_CHECK => 1 });
    }

# Find links which need isNew set, but isn't set (the Links table subclass
# already update's the Category table's Has_New_Links flag)
    $cond = GT::SQL::Condition->new(
        isNew    => '='  => 'No',
        Add_Date => '>=' => $date,
        VIEWABLE
    );
    my $new_rows = $links->count($cond);
    if ($new_rows) {
        $links->update({ isNew => 'Yes' }, $cond, { GT_SQL_SKIP_CHECK => 1 });
    }

# Update the category flags
    if ($new_rows or $old_rows or $opts->{reset}) {
        my $links_cl = $DB->table('Links', 'CatLinks');
        my $category = $DB->table('Category');
        $category->indexing(0);
        my %stalelinks = map { $_ => 1 } $category->select('ID', { Has_New_Links => 'Yes' })->fetchall_list;
        $links_cl->select_options('ORDER BY Add_Date ASC');
        my $sth = $links_cl->select('CategoryID', 'Add_Date', { isNew => 'Yes' }, VIEWABLE);
        my %updated;
        while (my ($cat_id, $date) = $sth->fetchrow) {
            next if exists $updated{$cat_id};
            my @ids = ($cat_id, @{$category->parents($cat_id)});
            $category->update(
                { Newest_Link => $date, Has_New_Links => 'Yes' },
                { ID => \@ids, Has_New_Links => 'No' },
                { GT_SQL_SKIP_CHECK => 1 }
            );
            for (@ids) {
                delete $stalelinks{$_};
                $updated{$_}++;
            }
        }
# Whatever's left in %stalelinks are the categories which don't have new links anymore
        $category->update({ Has_New_Links => 'No' }, { ID => [keys %stalelinks] }, { GT_SQL_SKIP_CHECK => 1 });
        $category->indexing(1);
    }
    $links->indexing(1);
    return $new_rows + $old_rows;
}
END_OF_SUB

$COMPILE{build_changed_flags} = __LINE__ . <<'END_OF_SUB';
sub build_changed_flags {
# ------------------------------------------------------------------
# Update isChanged flags, based on Mod_Date.
#
    Links::init_date();
    my $opts = shift || {};
    my $date = GT::Date::date_sub(GT::Date::date_get(), $CFG->{build_new_cutoff});
    my $links = $DB->table('Links');
    $links->indexing(0);
    my $cond = GT::SQL::Condition->new(
        isChanged => '=' => 'Yes',
        VIEWABLE,
        GT::SQL::Condition->new(
            Mod_Date => '<' => $date,
            isNew    => '=' => 'Yes',
            'OR'
        )
    );
# Unset isChanged for links which aren't 'new' anymore
    my $old_rows = $links->count($cond);
    if ($old_rows) {
        $links->update({ isChanged => 'No' }, $cond, { GT_SQL_SKIP_CHECK => 1 });
    }

# Find links which need isChanged set
    $cond = GT::SQL::Condition->new(
        isChanged => '='  => 'No',
        Mod_Date  => '>=' => $date,
        Add_Date  => '<'  => $date,
        VIEWABLE
    );
    my $new_rows = $links->count($cond);
    if ($new_rows) {
        $links->update({ isChanged => 'Yes' }, $cond, { GT_SQL_SKIP_CHECK => 1 });
    }

# Update the category flags
    if ($new_rows or $old_rows or $opts->{reset}) {
        my $category = $DB->table('Category');
        $category->indexing(0);
        my %stalelinks = map { $_ => 1 } $category->select('ID', { Has_Changed_Links => 'Yes' })->fetchall_list;
        my $sth = $DB->table('Links', 'CatLinks')->select('CategoryID', 'Add_Date', { isChanged => 'Yes' });
        my %updated;
        while (my ($cat_id, $date) = $sth->fetchrow_array) {
            next if exists $updated{$cat_id};
            my @ids = ($cat_id, @{$category->parents($cat_id)});
            $category->update(
                { Has_Changed_Links => 'Yes' },
                { ID => \@ids, Has_Changed_Links => 'No' },
                { GT_SQL_SKIP_CHECK => 1 }
            );
            for (@ids) {
                delete $stalelinks{$_};
                $updated{$_}++;
            }
        }
# Whatever's left in %stalelinks are the categories which don't have changed links anymore
        $category->update({ Has_Changed_Links => 'No' }, { ID => [keys %stalelinks] }, { GT_SQL_SKIP_CHECK => 1 });
        $category->indexing(1);
    }
    $links->indexing(1);
    return $new_rows + $old_rows;
}
END_OF_SUB

$COMPILE{build_cool_flags} = __LINE__ . <<'END_OF_SUB';
sub build_cool_flags {
# ------------------------------------------------------------------
# Update the isPopular flag determined on number of Hits.
#

# Work out based either on percentage, or top x links.
    my $link_db = $DB->table('Links');
    $GRAND_TOTAL ||= _grand_total();
    my $limit = int($CFG->{build_pop_cutoff} > 1 ? $CFG->{build_pop_cutoff} : $CFG->{build_pop_cutoff} * $GRAND_TOTAL) || 1;

# Select the popular links and update the flags if neccessary.
    $link_db->indexing(0);
    $link_db->select_options("ORDER BY Hits DESC", "LIMIT $limit");
    my $sth = $link_db->select(
        'ID', 'isPopular',
        GT::SQL::Condition->new(
            Hits => '>=' => 2,
            VIEWABLE
        )
    );
    my (@ids, @update_pop);
    while (my ($id, $ispop) = $sth->fetchrow) {
        push @ids, $id;
        push @update_pop, $id unless $ispop eq 'Yes';
    }
    $link_db->update({ isPopular => 'Yes' }, { ID => \@update_pop }, { GT_SQL_SKIP_CHECK => 1 })
        if @update_pop;

# Change any links that aren't popular back.
    my $cond = GT::SQL::Condition->new('isPopular', '=', 'Yes');
    $cond->add(ID => '!=' => \@ids) if @ids;
    $link_db->update({ isPopular => 'No' }, $cond);
    $link_db->indexing(1);
}
END_OF_SUB

$COMPILE{build_title} = __LINE__ . <<'END_OF_SUB';
sub build_title {
# ------------------------------------------------------------------
# Generate a title.  Unlike build_title_{un,}linked, returns something more
# useful: an array ref of hashes of Name and URL.
# The 2nd argument is an optional override URL for the last item.
#
    my ($input, $url) = @_;
    my $cat = $DB->table('Category');
    my $top = Links::language('LINKS_TOP');
    my @cats = ({ Name => $top, URL => "$CFG->{build_root_url}/" . ($CFG->{build_home} || ($CFG->{build_index_include} ? $CFG->{build_index} : '')) });

    my @si = split /\//, $input;
    for (0 .. $#si) {
        my $curl;
        if ($_ == 0 and $si[$_] eq Links::language('LINKS_NEW')) {
            $curl = "$CFG->{build_new_url}/";
        }
        elsif ($_ == $#si and $url) {
            $curl = $url;
        }
        else {
            $curl = "$CFG->{build_root_url}/" . $cat->as_url(join "/", @si[0 .. $_]) . "/" . ($CFG->{build_index_include} ? $CFG->{build_index} : '');
        }
        push @cats, { Name => $si[$_], URL => $curl };
    }

    return \@cats;
}
END_OF_SUB

$COMPILE{build_title_linked} = __LINE__ . <<'END_OF_SUB';
sub build_title_linked {
# ------------------------------------------------------------------
# Generate a linked title.
#
    my $input    = shift;
    my $complete = 0;
    my $home     = 1;
    if (ref $input) {
        $complete = $input->{complete} || 0;
        $home     = defined $input->{home} ? $input->{home} : 1;
        $input    = $input->{name};
    }

    my $db  = $DB->table('Category');
    my $top = Links::language('LINKS_TOP');

    my @dirs = split /\//, $input;
    my $last;
    $last = pop @dirs unless $complete;

    my @paths;
    push @paths, qq{<a href="$CFG->{build_root_url}/} . ($CFG->{build_home} || ($CFG->{build_index_include} ? $CFG->{build_index} : '')) . qq{">$top</a>} if $home;
    for (0 .. $#dirs) {
        my $path = "/" . $db->as_url(join "/", @dirs[0 .. $_]);
        push @paths, qq{<a href="$CFG->{build_root_url}$path/} . ($CFG->{build_index_include} ? $CFG->{build_index} : '') . qq{">$dirs[$_]</a>};
    }
    push @paths, $last unless $complete;
    return join ': ', @paths;
}
END_OF_SUB

$COMPILE{build_title_unlinked} = __LINE__ . <<'END_OF_SUB';
sub build_title_unlinked {
# --------------------------------------------------------
# Returns a string of the current category broken up by section.
# Useful for printing in the title.
#
    my $input = shift;
    my $output = join ': ', split /\//, $input;

    return $output;
}
END_OF_SUB

$COMPILE{build_toolbar} = __LINE__ . <<'END_OF_SUB';
sub build_toolbar {
# --------------------------------------------------------
# Create an Altavista style toolbar for the next and previous pages.
#
    my $opts = shift;

    my $root_url  = $opts->{url};
    my $first_url = $opts->{first_url} || "$root_url/" . ($CFG->{build_index_include} ? $CFG->{build_index} : '');
    my $next_url  = $opts->{next_url}  || "$root_url/$CFG->{build_more}";
    my $numhits   = $opts->{numlinks};
    my $nh        = $opts->{nh};
    my $maxhits   = $opts->{mh} || $CFG->{build_links_per_page};

    my ($next_hit, $prev_hit) = ($nh + 1, $nh - 1);

# First, set how many pages we have on the left and the right.
    my ($left, $right) = ($nh, int($numhits / $maxhits) - $nh);

# Then work out what page number we can go above and below.
    my $lower = $left > 7 ? $left - 7 : 1;
    my $upper = $right > 7 ? $nh + 7 : int($numhits/$maxhits) + 1;

# Finally, adjust those page numbers if we are near an endpoint.
    $upper += 8 - $nh if 7 - $nh >= 0;
    $lower -= $nh - int($numhits/$maxhits - 7) - 1 if $nh > $numhits / $maxhits - 7;

    my $url = "";
# Then let's go through the pages and build the HTML.
    $url .= $nh == 2 ? qq~<a href="$first_url">[<<]</a> ~ : qq~<a href="$next_url$prev_hit$CFG->{build_extension}">[<<]</a> ~
        if $nh > 1;

    for (my $i = 1; $i <= int($numhits/$maxhits) + 1; $i++) {
        if ($i < $lower) { $url .= " ... "; $i = $lower - 1; next }
        if ($i > $upper) { $url .= " ... "; last }
        $url .= $i == $nh
            ? "$i "
            : $i == 1
              ? qq~<a href="$first_url">$i</a> ~
              : qq~<a href="$next_url$i$CFG->{build_extension}">$i</a> ~;

        if ($i * $maxhits == $numhits) { $next_hit = $i if $nh == $i; last }
    }
    $url .= qq~<a href="$next_url$next_hit$CFG->{build_extension}">[>>]</a> ~
        unless $next_hit == $nh or $nh * $maxhits > $numhits;

    $url;
}
END_OF_SUB

$COMPILE{build_search_toolbar} = __LINE__ . <<'END_OF_SUB';
sub build_search_toolbar {
# --------------------------------------------------------
# Create an Altavista style toolbar for the next and previous pages.
#
    my $opts = shift;

    my $root_url = $opts->{url};
    my $numhits  = $opts->{numlinks};
    my $nh       = $opts->{nh};
    my $maxhits  = $opts->{mh} || $CFG->{build_links_per_page};

    my ($next_hit, $prev_hit) = ($nh + 1, $nh - 1);

# First, set how many pages we have on the left and the right.
    my ($left, $right) = ($nh, int($numhits / $maxhits) - $nh);

# Then work out what page number we can go above and below.
    my $lower = $left > 7 ? $left - 7 : 1;
    my $upper = $right > 7 ? $nh + 7 : int($numhits/$maxhits) + 1;

# Finally, adjust those page numbers if we are near an endpoint.
    $upper += 8 - $nh if 7 - $nh >= 0;
    $lower -= $nh - int($numhits/$maxhits - 7) - 1 if $nh > $numhits / $maxhits - 7;

    my $url = "";
# Then let's go through the pages and build the HTML.
    $url .= qq~<a href="$root_url;nh=$prev_hit">[<<]</a> ~ if $nh > 1;
    for (my $i = 1; $i <= int($numhits/$maxhits) + ($numhits % $maxhits ? 1 : 0); $i++) {
        if ($i < $lower) { $url .= " ... "; $i = $lower - 1; next }
        if ($i > $upper) { $url .= " ... "; last }
        $url .= $i == $nh ? "$i " : qq~<a href="$root_url;nh=$i">$i</a> ~;
        if ($i * $maxhits == $numhits) { $next_hit = $i if $nh == $i; last }
    }
    $url .= qq~<a href="$root_url;nh=$next_hit">[>>]</a> ~
        unless $next_hit == $nh or $nh * $maxhits > $numhits;

    $url;
}
END_OF_SUB

sub _compile {
# -------------------------------------------------------------------
# Compile will check %COMPILE for the subroutine, and if it exists compile
# it (with an eval).  Returns a code reference.
#
    my $sub = shift;
    if (exists $COMPILE{$sub}) {
        GT::AutoLoader::_compile(\%COMPILE, __PACKAGE__, $sub) if $COMPILE{$sub};
        return *{$Links::Build::{$sub}}{CODE};
    }
    else {
        die "Invalid method: $sub";
    }
}

sub _grand_total {
# -------------------------------------------------------------------
# Calculates the total in three queries as it can be significantly faster
# on large db's.
#

    # Create a category object just to make sure the db maintenance code has run:
    $DB->table('Category');

    my $total = $DB->table('CatLinks')->count - $DB->table('Links')->count({ isValidated => 'No' });

    if ($CFG->{payment}->{enabled}) {
        $total -= $DB->table('Links')->count(
            GT::SQL::Condition->new(
                ExpiryDate => '<' => time,
                isValidated => '=' => 'Yes'
            )
        );
    }

    return $total;
}

1;
