#!/usr/bin/perl
#------------------------------------------------------------------------------
#    mwForum - Web-based discussion forum
#    Copyright (c) 1999-2009 Markus Wichitill
#
#    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 my 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.
#------------------------------------------------------------------------------

use strict;
use warnings;
no warnings qw(uninitialized redefine);

# Imports
use MwfMain;

#------------------------------------------------------------------------------

# Init
my ($m, $cfg, $lng, $user, $userId) = MwfMain->new(@_);

# Check if access should be denied
$m->checkBan($userId) if $cfg->{banViewing};

# Get CGI parameters
my $topicId = $m->paramInt('tid');
my $targetPostId = $m->paramInt('pid');
my $page = $m->paramInt('pg');
my $showResults = $m->paramBool('results');
my $hilite = $m->paramStr('hl');
$topicId || $targetPostId or $m->error('errTPIdMiss');

# Get missing topicId from post
my $arcPfx = $m->{archive} ? 'arc_' : '';
if (!$topicId && $targetPostId) {
	$topicId = $m->fetchArray("
		SELECT topicId FROM ${arcPfx}posts WHERE id = ?", $targetPostId);
	$topicId or $m->error('errPstNotFnd');
}

# Get topic
my $topic = $m->fetchHash("
	SELECT topics.*, 
		topicReadTimes.lastReadTime
	FROM ${arcPfx}topics AS topics
		LEFT JOIN topicReadTimes AS topicReadTimes
			ON topicReadTimes.userId = :userId
			AND topicReadTimes.topicId = :topicId
	WHERE topics.id = :topicId",
	{ userId => $userId, topicId => $topicId });
$topic or $m->error('errTpcNotFnd');
$topic->{lastReadTime} ||= 0;
my $boardId = $topic->{boardId};

# Get board/category
my $board = $m->fetchHash("
	SELECT boards.*, 
		categories.id AS categId, categories.title AS categTitle
	FROM ${arcPfx}boards AS boards
		INNER JOIN categories AS categories
			ON categories.id = boards.categoryId
	WHERE boards.id = ?", $boardId);
$board or $m->error('errBrdNotFnd');
my $flat = $board->{flat};

# Shortcuts
my $autoCollapsing = !$flat && $user->{collapse};
my $showAvatars = $cfg->{avatars} && $user->{showAvatars};
my $stylePath = $m->{stylePath};

# Check if user can see and write to topic
my $boardAdmin = $user->{admin} || $m->boardAdmin($userId, $boardId);
my $topicAdmin = $board->{topicAdmins} && $m->fetchArray("
	SELECT userId = ? FROM posts WHERE topicId = ? AND parentId = 0", $userId, $topicId);
$boardAdmin || $topicAdmin || $m->boardVisible($board) or $m->error('errNoAccess');
my $boardWritable = $boardAdmin || $topicAdmin || $m->boardWritable($board, 1);

# Post rating shortcuts
my $postRating = $board->{rate} && @{$cfg->{postRatings}};
my $canRate = $postRating && $userId
	&& ($board->{rate} == 1 || $board->{rate} == 2 && ($boardAdmin || $topicAdmin));

# Print header
$m->printHeader($topic->{subject});

# Get minimal version of all topic posts
my $sameTopic = $topicId == $user->{lastTopicId};
my $topicReadTime = $sameTopic ? $user->{lastTopicTime} : $topic->{lastReadTime};
my $lowestUnreadTime = $m->max($topicReadTime, $user->{fakeReadTime}, 
	$m->{now} - $cfg->{maxUnreadDays} * 86400);
my $posts = $m->fetchAllHash("
	SELECT id, parentId,
		postTime > :prevOnTime AS new,
		postTime > :lowestUnreadTime AS unread
	FROM ${arcPfx}posts
	WHERE topicId = :topicId
	ORDER BY postTime", 
	{ prevOnTime => $user->{prevOnTime}, lowestUnreadTime => $lowestUnreadTime, 
		topicId => $topicId });

# Build post lookup tables and check if there are any new or unread posts
my %postsById = map(($_->{id} => $_), @$posts);  # Posts by id - hash of hashrefs
my %postsByParent = ();  # Posts by parent id - hash of arrayrefs of hashrefs
my $newPostsExist = 0;
my $unreadPostsExist = 0;
for my $post (@$posts) {
	push @{$postsByParent{$post->{parentId}}}, $post;
	$newPostsExist = 1 if $post->{new};
	$unreadPostsExist = 1 if $post->{unread};
}

# Determine page numbers and collect IDs of new or unread posts
my $postsPP = $m->min($user->{postsPP}, $cfg->{maxPostsPP}) || $cfg->{maxPostsPP};
my $postPos = 0;
my $firstUnrPostPage = undef;
my $firstNewPostPage = undef;
my $firstUnrPostId = undef;
my $firstNewPostId = undef;
my @newUnrPostIds = ();
my $basePost = $postsById{$topic->{basePostId}};
my $preparePost = sub {
	my $self = shift();
	my $postId = shift();

	# Shortcuts
	my $post = $postsById{$postId};
	
	# Assign page numbers to posts
	$post->{page} = int($postPos / $postsPP) + 1;

	# Set current page to a requested post's page	
	$page = $post->{page} if $postId == $targetPostId;

	# Determine first unread post and its page
	if (!$page && !$firstUnrPostPage && $post->{unread}) {
		$firstUnrPostPage = $post->{page};
		$firstUnrPostId = $postId;
	}

	# Determine first new post and its page
	if (!$page && !$firstNewPostPage && $post->{new}) {
		$firstNewPostPage = $post->{page};
		$firstNewPostId = $postId;
	}

	# Add new/unread post ID to list
	push @newUnrPostIds, $postId if $userId && ($post->{new} || $post->{unread});

	# Recurse through children
	$postPos++;
	for my $child (@{$postsByParent{$postId}}) {
		$child->{id} != $postId or $m->error("Post is its own parent?!");
		$self->($self, $child->{id});
	}
};
$preparePost->($preparePost, $basePost->{id});

# Jump to first unread post if available, to first new post otherwise
my $firstNewUnrPostId = undef;
if ($userId && !$page) {
	$page = $firstUnrPostPage || $firstNewPostPage ;
	$firstNewUnrPostId = $firstUnrPostId || $firstNewPostId;
}

# Get the full content of those posts that are on the current page
# Note: full posts are not copied to @$posts and %postsByParent
$page ||= 1;
my @pagePostIds = map($_->{page} == $page ? $_->{id} : (), @$posts);
@pagePostIds or $m->error('errPstNotFnd');
my $ignoreStr = $userId ? ", 
		userIgnores.userId IS NOT NULL AS ignored" : "";
my $ignoreJoin = $userId ? "
		LEFT JOIN userIgnores AS userIgnores
			ON userIgnores.userId = :userId
			AND userIgnores.ignoredId = posts.userId" : "";
my $ratingsStr = $postRating ? ", 
		postRatings.userId IS NOT NULL AS rated" : "";
my $ratingsJoin = $postRating ? "
		LEFT JOIN postRatings AS postRatings
			ON postRatings.postId = posts.id
			AND postRatings.userId = :userId" : "";
my $pagePosts = $m->fetchAllHash("
	SELECT posts.*, 
		posts.postTime > :prevOnTime AS new,
		posts.postTime > :lowestUnreadTime AS unread,
		users.userName, users.title AS userTitle, 
		users.postNum AS userPostNum,	users.avatar, users.signature, 
		users.openId, users.privacy
		$ignoreStr
		$ratingsStr
	FROM ${arcPfx}posts AS posts
		LEFT JOIN users AS users
			ON users.id = posts.userId
		$ignoreJoin
		$ratingsJoin
	WHERE posts.id IN (:pagePostIds)",
	{ userId => $userId, prevOnTime => $user->{prevOnTime}, lowestUnreadTime => $lowestUnreadTime,
		pagePostIds => \@pagePostIds });
my %pageUserIds = ();
for my $post (@$pagePosts) { 
	$post->{page} = $page;
	$postsById{$post->{id}} = $post;
	$pageUserIds{$post->{userId}} = 1;
}
$basePost = $postsById{$topic->{basePostId}};

# Remove ignored and base crosslink posts from @newUnrPostIds
if ($userId) {
	@newUnrPostIds = grep(!$postsById{$_}{ignored}, @newUnrPostIds);
	shift @newUnrPostIds if $postsById{$newUnrPostIds[0]}{userId} == -2;
}

# Mark branches that shouldn't be auto-collapsed
if ($autoCollapsing) {
	for (@newUnrPostIds) {
		my $post = $postsById{$_};
		while ($post = $postsById{$post->{parentId}}) {
			last if $post->{noCollapse};
			$post->{noCollapse} = 1;
		}
	}

	if ($targetPostId) {
		my $post = $postsById{$targetPostId};
		while ($post = $postsById{$post->{parentId}}) {
			last if $post->{noCollapse};
			$post->{noCollapse} = 1;
		}
	}
}

# Get poll
my $poll = undef;
my $polls = $cfg->{polls};
my $pollId = $topic->{pollId};
my $canPoll = ($polls == 1 || $polls == 2 && ($boardAdmin || $topicAdmin))
	&& ($userId && $userId == $basePost->{userId} || $boardAdmin);
$poll = $m->fetchHash("
	SELECT * FROM polls WHERE id = ?", $pollId)
	if $polls && $pollId;

# Get attachments
if ($cfg->{attachments} && $board->{attach}) {
	my $attachments = $m->fetchAllHash("
		SELECT * 
		FROM attachments
		WHERE postId IN (:pagePostIds)
		ORDER BY webImage, id",
		{ pagePostIds => \@pagePostIds });
	push @{$postsById{$_->{postId}}{attachments}}, $_ for @$attachments;
}

# Get user badges
my @badges = ();
my %userBadges = ();
if (@{$cfg->{badges}} && $user->{showDeco}) {
	for my $line (@{$cfg->{badges}}) {
		my ($id, $smallIcon, $title) = $line =~ /(\w+)\s+\w+\s+(\S+)\s+\S+\s+"([^"]+)"/;
		push @badges, [ $id, $title, $smallIcon ] if $smallIcon ne '-';
	}
	my @pageUserIds = keys(%pageUserIds);
	my $userBadges = $m->fetchAllArray("
		SELECT userId, badge FROM userBadges WHERE userId IN (:pageUserIds)",
		{ pageUserIds => \@pageUserIds });
	push @{$userBadges{$_->[0]}}, $_->[1] for @$userBadges;
}

# GeoIP
my $geoIp = undef;
my %geoCache;
if ($cfg->{geoIp} && $user->{showDeco}) {
	if (eval { require Geo::IP }) {
		$geoIp = Geo::IP->open($cfg->{geoIp});
	}
	elsif (eval { require Geo::IP::PurePerl }) {
		$geoIp = Geo::IP::PurePerl->open($cfg->{geoIp});
	}
}

# Google highlighting
my ($googleWords) = $m->{env}{referrer} =~ /www\.google.*?q=(.*?)(?:&|$)/;
if ($googleWords && $googleWords !~ /^related:/) {
	$googleWords =~ s!\+! !g;
	$googleWords =~ s!%([0-9a-fA-F]{2})!chr hex $1!eg;
	$hilite = $googleWords;
}

# Highlighting
my @hiliteWords = ();
if ($hilite) {
	# Split string and weed out stuff that could break entities
	my $hiliteRxEsc = $hilite;
	$hiliteRxEsc =~ s!([\\\$\[\](){}.*+?^|-])!\\$1!g;
	@hiliteWords = split(' ', $hiliteRxEsc);
	@hiliteWords = grep(length > 2, @hiliteWords);
	@hiliteWords = grep(!/^(?:amp|quot|quo|uot|160)\z/, @hiliteWords);
}

# Page links
my $postNum = $topic->{postNum};
my @pageLinks = ();
my $pageNum = int($postNum / $postsPP) + ($postNum % $postsPP != 0);
if ($pageNum > 1) {
	my $maxPageNum = $m->min($pageNum, 8);
	push @pageLinks, { url => $m->url('topic_show', tid => $topicId, pg => $_), 
		txt => $_, dsb => $_ == $page }
		for 1 .. $maxPageNum;
	push @pageLinks, { txt => "..." }
		if $maxPageNum < $pageNum - 1;
	push @pageLinks, { url => $m->url('topic_show', tid => $topicId, pg => $pageNum), 
		txt => $pageNum, dsb => $pageNum == $page }
		if $maxPageNum < $pageNum;
	push @pageLinks, { url => $m->url('topic_show', tid => $topicId, pg => $page - 1), 
		txt => 'comPgPrev', dsb => $page == 1 };
	my $url = $cfg->{seoRewrite} && !$userId ? "topic_${topicId}_" . ($page + 1) . ".html"
		: $m->url('topic_show', tid => $topicId, pg => $page + 1);
	push @pageLinks, { url => $url, txt => 'comPgNext', dsb => $page == $pageNum };
}

# Navigation button links
my @navLinks = ();
push @navLinks, { url => $m->url('prevnext', tid => $topicId, dir => 'prev'), 
	txt => 'tpcPrev', ico => 'prev' };
push @navLinks, { url => $m->url('prevnext', tid => $topicId, dir => 'next'), 
	txt => 'tpcNext', ico => 'next' };
push @navLinks, { url => $m->url('board_show', tid => $topicId, tgt => "tid$topicId"), 
	txt => 'comUp', ico => 'up' };

# User button links
my @userLinks = ();
if (!$m->{archive}) {
	push @userLinks, { url => $m->url('poll_add', tid => $topicId), txt => 'tpcPolAdd', ico => 'poll' }
		if !$poll && $canPoll && (!$topic->{locked} || $boardAdmin || $topicAdmin);
	push @userLinks, { url => $m->url('topic_tag', tid => $topicId), txt => 'tpcTag', ico => 'tag' }
		if ($userId && $userId == $basePost->{userId} || $boardAdmin || $topicAdmin)
		&& ($cfg->{allowTopicTags} == 2 || $cfg->{allowTopicTags} == 1 && ($boardAdmin || $topicAdmin));
	push @userLinks, { url => $m->url('topic_subscribe', tid => $topicId), 
		txt => 'tpcSubs', ico => 'subscribe' }
		if $userId && $cfg->{subscriptions};
	push @userLinks, { url => $m->url('topic_ratings', tid => $topicId), 
		txt => 'tpcRatings', ico => 'rate' }
		if $cfg->{linkAvgRatPgs} && $board->{rate};
	push @userLinks, { url => $m->url('forum_overview', act => 'new', tid => $topicId), 
		txt => 'comShowNew', ico => 'shownew' }
		if $userId && $newPostsExist;
	push @userLinks, { url => $m->url('forum_overview', act => 'unread', tid => $topicId, 
		time => $lowestUnreadTime), txt => 'comShowUnr', ico => 'showunread' }
		if $userId && $unreadPostsExist;
	$m->callPlugin($cfg->{includePlg}{topicUserLink}, links => \@userLinks, 
		board => $board, topic => $topic);
}
	
# Admin button links	
my @adminLinks = ();
if (($boardAdmin || $topicAdmin) && !$m->{archive}) {
	push @adminLinks, { url => $m->url('topic_stick', tid => $topicId, 
		act => $topic->{sticky} ? 'unstick' : 'stick', auth => 1), 
		txt => $topic->{sticky} ? 'tpcAdmUnstik' : 'tpcAdmStik', ico => 'stick' }
		if $boardAdmin;
	push @adminLinks, { url => $m->url('topic_lock', tid => $topicId, 
		act => $topic->{locked} ? 'unlock' : 'lock', auth => 1), 
		txt => $topic->{locked} ? 'tpcAdmUnlock' : 'tpcAdmLock', ico => 'lock' };
	push @adminLinks, { url => $m->url('topic_move', tid => $topicId), 
		txt => 'tpcAdmMove', ico => 'move' }
		if $boardAdmin;
	push @adminLinks, { url => $m->url('topic_merge', tid => $topicId), 
		txt => 'tpcAdmMerge', ico => 'merge' }
		if $boardAdmin;
	push @adminLinks, { url => $m->url('user_confirm', script => 'topic_delete', tid => $topicId,
		notify => ($basePost->{userId} != $userId ? 1 : 0), name => $topic->{subject}), 
		txt => 'tpcAdmDelete', ico => 'delete' };
	$m->callPlugin($cfg->{includePlg}{topicAdminLink}, links => \@adminLinks,
		board => $board, topic => $topic);
}

# Print page bar
my $url = $m->url('forum_show', tgt => "bid$boardId");
my $categStr = "<a href='$url'>$board->{categTitle}</a> / ";
$url = $m->url('board_show', tid => $topicId, tgt => "tid$topicId");
my $boardStr = "<a href='$url'>$board->{title}</a> / ";
my $lockStr = $topic->{locked} ? " $lng->{tpcLocked}" : "";
my $pageStr = $page > 1 ? " ($lng->{comPgTtl} $page)" : "";
my $hitStr = $cfg->{topicHits} ? " ($topic->{hitNum} $lng->{tpcHits})" : "";
$m->printPageBar(
	mainTitle => $lng->{tpcTitle}, 
	subTitle => $categStr . $boardStr . $topic->{subject} . $lockStr . $pageStr . $hitStr, 
	navLinks => \@navLinks, pageLinks => \@pageLinks, userLinks => \@userLinks, 
	adminLinks => \@adminLinks);

# Print spacer ($bvckup)
	print
		"<div class=spacer></div>\n\n";

# Print poll
if ($poll && $polls && !$m->{archive}) {
	# Print poll header
	my $lockedStr = $poll->{locked} ? $lng->{tpcPolLocked} : "";
	print
		"<div class='frm pol'>\n",
		"<div class='hcl'>\n",
		"<span class='htt'>$lng->{tpcPolTtl}</span>\n",
		"$poll->{title} $lockedStr\n",
		"</div>\n",
		"<div class='ccl'>\n";

	# Check if user already voted
	my $voted = $m->fetchArray("
		SELECT 1 FROM pollVotes WHERE pollId = ? AND userId = ?", $pollId, $userId) ? 1 : 0;

	# Print results
	if ($voted || $poll->{multi} || $showResults || !$userId || !$boardWritable 
		|| $topic->{locked} || $poll->{locked}) {

		my $options = undef;
		my $voteSum = undef;

		if ($poll->{locked}) {
			# Get consolidated results
			$options = $m->fetchAllHash("
				SELECT id, title, votes FROM pollOptions WHERE pollId = ? ORDER BY id", $pollId);

			# Get sum of votes
			$voteSum = $m->fetchArray("
				SELECT SUM(votes) FROM pollOptions WHERE pollId = ?", $pollId) || 1;
		}
		else {
			# Get results from votes
			$options = $m->fetchAllHash("
				SELECT pollOptions.id, pollOptions.title,
					COUNT(pollVotes.optionId) AS votes
				FROM pollOptions AS pollOptions
					LEFT JOIN pollVotes AS pollVotes
						ON pollVotes.pollId = :pollId
						AND pollVotes.optionId = pollOptions.id
				WHERE pollOptions.pollId = :pollId
				GROUP BY pollOptions.id, pollOptions.title",
				{ pollId => $pollId });

			# Get sum of votes
			$voteSum = $m->fetchArray("
				SELECT COUNT(*) FROM pollVotes WHERE pollId = ?", $pollId) || 1;
		}

		# Print results
		print	"<table class='plr'>\n";

		for my $option (@$options) {
			my $votes = $option->{votes};
			my $percent = sprintf("%.0f", $votes / $voteSum * 100);
			my $width = $percent * 4;
			print	
				"<tr>\n",
				"<td class='plo'>$option->{title}</td>\n",
				"<td class='plv'>$votes</td>\n",
				"<td class='plp'>$percent\%</td>\n",
				"<td class='plg'><img class='plb' src='$stylePath/poll_bar.png'",
				" style='width: ${width}px' alt=''/></td>\n",
				"</tr>\n";
		}

		print	"</table>\n";
	}
	
	# Print poll form
	if ((!$voted || $poll->{multi})
		&& (!$showResults && $userId && $boardWritable 
		&& !$topic->{locked} && !$poll->{locked})) {
			
		# Get poll options
		my $options = $m->fetchAllHash("
			SELECT id, title FROM pollOptions WHERE pollId = ? ORDER BY id", $pollId);
		
		# Get user's votes to disable options in multi-vote polls
		my $votes = $m->fetchAllArray("
			SELECT optionId FROM pollVotes WHERE pollId = ? AND userId = ?", $pollId, $userId);
		
		print
			"<form action='poll_vote$m->{ext}' method='post'>\n",
			"<div>\n";
	
		# Print poll options
		for my $option (@$options) {
			my $disabled = "";
			for my $vote (@$votes) { 
				$disabled = "disabled='disabled' checked='checked'", last if $vote->[0] == $option->{id} 
			}

			print $poll->{multi} 
				? "<label><input type='checkbox' name='option_$option->{id}' $disabled/> " .
					"$option->{title}</label><br/>\n"
				: "<label><input type='radio' name='option' value='$option->{id}'/> " .
					"$option->{title}</label><br/>\n";
		}

		$url = $m->url('topic_show', tid => $topicId, results => 1);	
		print
			"<br/>",
			$m->submitButton('tpcPolVote', 'poll'),
			$poll->{multi} ? "" 
				: "<a href='$url'>$lng->{tpcPolShwRes}</a>\n",
			"<input type='hidden' name='tid' value='$topicId'/>\n",
			$m->stdFormFields(),
			"</div>\n",
			"</form>\n";
	}

	print "</div>\n";

	# Print lock poll button
	my @btlLines = ();
	if ($canPoll && !$poll->{locked}) {
		$url = $m->url('poll_lock', tid => $topicId, auth => 1);
		push @btlLines, "<a href='$url' title='$lng->{tpcPolLockTT}'>$lng->{tpcPolLock}</a>\n";
	}

	# Print delete poll button
	if ($canPoll && (!$poll->{locked} || $boardAdmin || $topicAdmin)) {
		$url = $m->url('user_confirm', tid => $topicId, pollId => $pollId, script => 'poll_delete',
			auth => 1, name => $poll->{title});
		push @btlLines, "<a href='$url' title='$lng->{tpcPolDelTT}'>$lng->{tpcPolDel}</a>\n";
	}

	# Print button cell if not empty
	print "<div class='bcl'>\n", @btlLines, "</div>\n" if @btlLines;
	print "</div>\n\n";
}

# Determine position number of first and last posts on current page
my $firstPostPos = $postsPP * ($page - 1);
my $lastPostPos = $postsPP ? $postsPP * $page - 1 : @$posts - 1;

# Recursively print posts
$postPos = 0;
my $printPost = sub {
	my $self = shift();
	my $postId = shift();
	my $depth = shift();

	# Shortcuts
	my $post = $postsById{$postId};
	my $postUserId = $post->{userId};
	my $childNum = @{$postsByParent{$postId}};

	# Branch collapsing flags
	my $printBranchToggle = !$flat && $childNum && $post->{page} == $page;
	my $collapsed = $autoCollapsing && @newUnrPostIds && !$post->{noCollapse} ? 1 : 0;

	# Print if on current page
	if ($post->{page} == $page) {
		# Shortcuts
		my $parentId = $post->{parentId};
		my $indent = $flat ? 0 : $m->min(70, $user->{indent} * $depth);

		# Print post
		if ($post->{approved} || $boardAdmin || $topicAdmin || $userId && $userId == $postUserId) {
			# Format times
			my $postTimeStr = $m->formatTime($post->{postTime}, $user->{timezone});
			my $editTimeStr = undef;
			if ($post->{editTime}) {
				$editTimeStr = $m->formatTime($post->{editTime}, $user->{timezone});
				$editTimeStr = "<em>$editTimeStr</em>" 
					if $post->{editTime} > $user->{prevOnTime} && !$post->{new};
				$editTimeStr = "<span class='htt'>$lng->{tpcEdited}</span> $editTimeStr\n";
			}
			
			# Format username
			$url = $m->url('user_info', uid => $postUserId);
			my $userNameStr = $post->{userName} || $post->{userNameBak} || " - ";
			$userNameStr = "<a href='$url'>$userNameStr</a>" if $postUserId > 0;
			$userNameStr = "<a href='$post->{openId}'><img class='ico'"
				. " src='$cfg->{dataPath}/icon_openid.png' title='OpenID: $post->{openId}'/></a> " 
				. $userNameStr 
				if $post->{openId};
			$userNameStr .= " " . $m->formatUserTitle($post->{userTitle})
				if $post->{userTitle} && $user->{showDeco};
			$userNameStr .= " " . $m->formatUserRank($post->{userPostNum})
				if @{$cfg->{userRanks}} && !$post->{userTitle} && $user->{showDeco};
			
			# Format user badges
			if (@badges && $userBadges{$postUserId} && $user->{showDeco}) {
				for my $badge (@badges) {
					for my $userBadge (@{$userBadges{$postUserId}}) {
						if ($userBadge eq $badge->[0]) {
							$userNameStr .= " <img class='ubs' src='$cfg->{dataPath}/$badge->[2]'"
								. " title='$badge->[1]' alt=''/>";
							last;
						}
					}
				}
			}
			
			# Format GeoIP flag
			if ($geoIp && (!$post->{privacy} || $user->{admin})) {
				my $code = $geoCache{$post->{ip}};
				if (!defined($code)) {
					$code = lc($geoIp->country_code_by_addr($post->{ip})) || "";
					$geoCache{$post->{ip}} = $code;
				}
				$code = "" if $code && $code eq $cfg->{geoIpSkip};
				if ($code) {
					my $country = $geoCache{$code};
					if (!defined($country)) {
						$country = $geoIp->country_name_by_addr($post->{ip}) || "";
						$country =~ s!'!&#39;!g;
						$geoCache{$code} = $country;
					}
					$userNameStr .= " <img class='flg' src='$cfg->{dataPath}/flags/$code.png'"
					. " alt='[$code]' title='$country'/>";
				}
			}

			# Format values for ignored posts
			my $ignoredCss = $post->{ignored} ? "style='display: none'" : "";
			my $ignoredJs = $post->{ignored} ? "onclick='mwfShowIgnored($postId)'" : "";
			$post->{signature} = "" if $post->{ignored};
			
			# Format misc values
			$m->dbToDisplay($board, $post);
			my $pstClasses = "frm pst";
			$pstClasses .= " new" if $userId && $post->{new};
			$pstClasses .= " unr" if $userId && $post->{unread};
			$pstClasses .= " tgt" if $postId == $targetPostId;

			# Format invisible and locked post icons
			my $invisImg = !$post->{approved} ? " <img class='ico' src='$m->{stylePath}/post_i.png'"
				. " title='$lng->{tpcInvisTT}' alt='$lng->{tpcInvis}'/> " : "";
			my $lockImg = $post->{locked} ? " <img class='ico' src='$m->{stylePath}/topic_l.png'"
				. " title='$lng->{tpcLockdTT}' alt='$lng->{tpcLockd}'/> " : "";
				
			# Highlight search keywords
			if (@hiliteWords) {
				my $body = ">$post->{body}<";
				$body =~ s|>(.*?)<|
					my $text = $1;
					eval { $text =~ s!($_)!<em>$1</em>!gi } for @hiliteWords;
					">$text<";
				|egs;
				$post->{body} = substr($body, 1, -1);
			}

			# Determine variable post icon attributes
			my $imgSrc; 
			my $imgTitle = "";
			my $imgAlt = "";
			if (!$userId) { $imgSrc = "post_ou" }
			else {
				if ($post->{new} && $post->{unread}) { 
					$imgSrc = "post_nu"; $imgTitle = $lng->{comNewUnrdTT}; $imgAlt = $lng->{comNewUnrd};
				}
				elsif ($post->{new}) { 
					$imgSrc = "post_nr"; $imgTitle = $lng->{comNewReadTT}; $imgAlt = $lng->{comNewRead};
				}
				elsif ($post->{unread}) { 
					$imgSrc = "post_ou"; $imgTitle = $lng->{comOldUnrdTT}; $imgAlt = $lng->{comOldUnrd};
				}
				else { 
					$imgSrc = "post_or"; $imgTitle = $lng->{comOldReadTT}; $imgAlt = $lng->{comOldRead};
				}
			}
			my $imgAttr = "src='$stylePath/$imgSrc.png' title='$imgTitle' alt='$imgAlt'";

			# Print post header
			print
				"<div class='$pstClasses' id='pid$postId' style='margin-left: $indent%' $ignoredJs>\n",
				"<div class='hcl'>\n",
				"<span class='nav'>\n";

			# Print navigation buttons
			if (!$flat) {
				if (($post->{unread} || $post->{new} || $postPos == $firstPostPos) 
					&& @newUnrPostIds && @newUnrPostIds < $postNum && $postNum > 2 
					&& $postId != $newUnrPostIds[-1]) {
						
					# Print goto next new/unread post button
					my $nextPostId = undef;
					if ($postPos == 0) { $nextPostId = $newUnrPostIds[0] }
					else {
						for my $i (0 .. @newUnrPostIds) { 
							if ($newUnrPostIds[$i] == $postId) {
								$nextPostId = $newUnrPostIds[$i+1];
								last;
							}
						}
					}
					if ($nextPostId) {
						$url = $postsById{$nextPostId}{page} == $page 
							? "#pid$nextPostId" : $m->url('topic_show', pid => $nextPostId);
						print
							"<a href='$url'><img class='ico' src='$stylePath/post_nn.png'",
							" title='$lng->{tpcNxtPstTT}' alt='$lng->{tpcNxtPst}'/></a>\n";
					}
				}

				# Print jump to parent post button
				$url = $postsById{$parentId}{page} == $page	
					? "#pid$parentId" : $m->url('topic_show', pid => $parentId);
				print
					"<a href='$url'><img class='ico' src='$stylePath/nav_up.png'",
					" title='$lng->{tpcParentTT}' alt='$lng->{tpcParent}'/></a>\n"
					if $parentId;
			}
			elsif ($postPos == 0 && @newUnrPostIds && @newUnrPostIds < $postNum && $postNum > 2) {
				# Print one goto new/unread post button in non-threaded boards
				my ($nextPostId) = @newUnrPostIds;
				my $url = $postsById{$nextPostId}{page} == $page 
					? "#pid$nextPostId" : $m->url('topic_show', pid => $nextPostId);
				print
					"<a href='$url'><img class='ico' src='$stylePath/post_nn.png'",
					" title='$lng->{tpcNxtPstTT}' alt='$lng->{tpcNxtPst}'/></a>\n";
			}

			print "</span>\n";

			# Print branch toggle icon
			if ($printBranchToggle) {
				my $img = $collapsed ? 'nav_plus' : 'nav_minus';
				my $alt = $collapsed ? '+' : '-';
				print 
					"<img class='ico' id='tgl$postId' src='$stylePath/$img.png'",
					" onclick='mwfToggleBranch($postId)' ondblclick='mwfExpandAllBranches()'",
					" title='$lng->{tpcBrnCollap}' alt='$alt'/>\n";
			}

			# Print icon and main header items
			$url = $m->url('topic_show', pid => $postId, tgt => "pid$postId");
			print
				"<a href='$url'><img class='ico' $imgAttr/></a>\n",
				$lockImg,
				$invisImg,
				$postUserId > -2 ? "<span class='htt'>$lng->{tpcBy}</span> $userNameStr\n" : "",
				"<span class='htt'>$lng->{tpcOn}</span> $postTimeStr\n", 
				$editTimeStr;
			
			# Print IP
			print "<span class='htt'>IP</span> $post->{ip}\n" 
				if $cfg->{showPostIp} == 1 && $boardAdmin
				|| $cfg->{showPostIp} == 2 && $userId;
			
			# Print post rating
			if ($postRating && $post->{rating}) {
				my $rating = $post->{rating} / 100;
				print 
					"<span class='htt'>$lng->{tpcRating}</span> $rating ",
					$user->{showDeco} ? $m->formatPostRating($rating) . "\n" : "\n";
			}
			
			print "</div>\n<div class='ccl' $ignoredCss>\n";

			# Print avatar
			if ($showAvatars && index($post->{avatar}, "gravatar:") == 0) {
				my $md5 = $m->md5(substr($post->{avatar}, 9));
				my $url = "http://gravatar.com/avatar/$md5?s=$cfg->{avatarWidth}";
				print "<img class='ava' src='$url' alt=''/>\n";
			}
			elsif ($showAvatars && $post->{avatar}) {
				print	"<img class='ava' src='$cfg->{attachUrlPath}/avatars/$post->{avatar}' alt=''/>\n";
			}

			# Print body
			print $post->{body}, "\n</div>\n";

			# Print reply button
			my @btlLines = ();
			if (($boardWritable && !$topic->{locked} && !$post->{locked} || $boardAdmin || $topicAdmin) 
				&& ($postUserId != -2 || $postId == $basePost->{id})) {
				$url = $m->url('post_add', pid => $postId);
				push @btlLines, $m->buttonLink($url, 'tpcReply', 'write');
			}

			# Print reply with quote button
			if (($boardWritable && !$topic->{locked} && !$post->{locked} || $boardAdmin || $topicAdmin)
				&& $cfg->{quote} && ($flat || $cfg->{quote} == 2)
				&& $postUserId != -2) {
				$url = $m->url('post_add', pid => $postId, quote => 1);
				push @btlLines, $m->buttonLink($url, 'tpcQuote', 'write');
			}

			# Print edit button
			if ($userId 
				&& ($userId == $postUserId && !$topic->{locked} 
				&& !$post->{locked} || $boardAdmin || $topicAdmin)
				&& !($postUserId == -2 && $postId != $basePost->{id})) {
				$url = $m->url('post_edit', pid => $postId);
				push @btlLines, $m->buttonLink($url, 'tpcEdit', 'edit');
			}

			# Print delete button
			if ($userId 
				&& ($userId == $postUserId && !$topic->{locked} 
				&& !$post->{locked} || $boardAdmin || $topicAdmin)) {
				$url = $m->url('user_confirm', script => 'post_delete', pid => $postId, 
					notify => ($postUserId != $userId ? 1 : 0), name => $postId);
				push @btlLines, $m->buttonLink($url, 'tpcDelete', 'delete');
			}

			# Print attach button
			if ($cfg->{attachments} && $userId && $postUserId != -2
				&& ($userId == $postUserId && !$topic->{locked} 
				&& !$post->{locked} || $boardAdmin || $topicAdmin)
				&& ($board->{attach} == 1 || $board->{attach} == 2 && $boardAdmin)) {
				$url = $m->url('post_attach', pid => $postId);
				push @btlLines, $m->buttonLink($url, 'tpcAttach', 'attach');
			}

			# Print report button
			if ($cfg->{reports} && $userId && $userId != $postUserId && $postUserId != -2) {
				$url = $m->url('report_add', pid => $postId);
				push @btlLines, $m->buttonLink($url, 'tpcReport', 'report');
			}

			# Print rate button
			if ($canRate && !$post->{rated} && $postUserId != -2
				&& (!$topic->{locked} && !$post->{locked} || $boardAdmin || $topicAdmin)) {
				$url = $m->url('post_rate', pid => $postId);
				push @btlLines, $m->buttonLink($url, 'tpcRate', 'rate');
			}

			# Print approve button
			if (!$post->{approved} && ($boardAdmin || $topicAdmin)) {
				$url = $m->url('post_approve', pid => $postId, auth => 1);
				push @btlLines, $m->buttonLink($url, 'tpcApprv', 'approve');
			}

			# Print lock/unlock button
			if (!$post->{locked} && ($boardAdmin || $topicAdmin)) {
				$url = $m->url('post_lock', pid => $postId, act => 'lock', auth => 1);
				push @btlLines, $m->buttonLink($url, 'tpcLock', 'lock');
			}
			elsif ($boardAdmin || $topicAdmin) {
				$url = $m->url('post_lock', pid => $postId, act => 'unlock', auth => 1);
				push @btlLines, $m->buttonLink($url, 'tpcUnlock', 'lock');
			}

			# Print branch button
			if ($post->{parentId} && $postUserId != -2 && ($boardAdmin || $topicAdmin)) {
				$url = $m->url('branch_admin', pid => $postId);
				push @btlLines, $m->buttonLink($url, 'tpcBranch', 'branch');
			}

			# Print include plugin buttons
			$m->callPlugin($cfg->{includePlg}{postLink}, lines => \@btlLines, board => $board, 
				topic => $topic, post => $post, boardAdmin => $boardAdmin, topicAdmin => $topicAdmin) 
				if $cfg->{includePlg}{postLink};

			# Print button cell if there're button links
			print "<div class='bcl' $ignoredCss>\n", @btlLines, "</div>\n" if @btlLines && !$m->{archive};
			print "</div>\n\n";
		}
		else {
			# Print unapproved post bar
			print
				"<div class='frm hps' style='margin-left: $indent%'>\n",
				"<div class='hcl'>\n",
				"<a id='pid$postId'></a>\n",
				"$lng->{tpcHidTtl} $lng->{tpcHidUnappr}\n",
				"</div>\n",
				"</div>\n\n";
		}
	}

	# Print div for branch collapsing
	if ($printBranchToggle) {
		print "<div class='brn' id='brn$postId'>";
		print "<script type='text/javascript'>",
			"document.getElementById('brn$postId').style.display = 'none'</script>" 
			if $collapsed;
		print "\n";
	}
	
	# Print children recursively
	$postPos++;
	for my $child (@{$postsByParent{$postId}}) {
		return if $postPos > $lastPostPos && !$printBranchToggle;
		$child->{id} != $postId or $m->error("Post is its own parent?!");
		$self->($self, $child->{id}, $depth + 1);
	}

	print "</div>\n" if $printBranchToggle;
};
$printPost->($printPost, $basePost->{id}, 0);

# Repeat page bar
# $bvckup $m->printPageBar(repeat => 1);

# Print Javascript
my $scrollPostId = $targetPostId || $firstNewUnrPostId || 0;
print <<"EOSCRIPT";
<script type='text/javascript'>$m->{cdataStart}
	function mwfScrollOnLoad() {
		var elem = document.getElementById('pid$scrollPostId');
		if (elem) window.scrollTo(0, elem.offsetTop - 5);
	}

	if ($scrollPostId && $scrollPostId != $basePost->{id} && !$showResults) {
		if (window.addEventListener) window.addEventListener('load', mwfScrollOnLoad, false);
		else if (window.attachEvent) window.attachEvent('onload', mwfScrollOnLoad);
	}

	function mwfToggleBranch(postId) {
		var branch = document.getElementById('brn' + postId);
		var toggle = document.getElementById('tgl' + postId);
		if (!branch || !toggle) return;
		if (branch.style.display != 'none') {
			branch.style.display = 'none';
			toggle.src = '$stylePath/nav_plus.png';
			toggle.title = '$lng->{tpcBrnExpand}';
			toggle.alt = '+';
		}
		else {
			branch.style.display = '';
			toggle.src = '$stylePath/nav_minus.png';
			toggle.title = '$lng->{tpcBrnCollap}';
			toggle.alt = '-';
		}
	}

	function mwfExpandAllBranches() {
		var divs = document.getElementsByTagName('div');
		for (var i=0; i < divs.length; i++) {
			if (divs[i].id.indexOf('brn') == 0) divs[i].style.display = '';
		}
		var imgs = document.getElementsByTagName('img');
		for (var i=0; i < imgs.length; i++) {
			if (imgs[i].id.indexOf('tgl') == 0) {
				imgs[i].src = '$stylePath/nav_minus.png';
				imgs[i].title = '$lng->{tpcBrnCollap}';
				imgs[i].alt = '-';
			}
		}
	}

	function mwfShowIgnored(postId) {
		var post = document.getElementById('pid' + postId);
		if (!post) return;
		var nodes = post.childNodes;
		for (var i=0; i < nodes.length; i++) {
			if (nodes[i].className && nodes[i].className.indexOf('ccl') > -1) nodes[i].style.display = '';
			if (nodes[i].className && nodes[i].className.indexOf('bcl') > -1) nodes[i].style.display = '';
		}
	}
$m->{cdataEnd}</script>\n
EOSCRIPT

# Update topic read data
if ($userId && !$sameTopic && !$m->{archive}) {
	if ($topic->{lastPostTime} > $lowestUnreadTime) {
		# Replace topic's last read time
		$m->dbDo("
			DELETE FROM topicReadTimes WHERE topicId = ? AND userId = ?", $topicId, $userId);
		$m->dbDo("
			INSERT INTO topicReadTimes (topicId, userId, lastReadTime) VALUES (?, ?, ?)",
			$topicId, $userId, $m->{now});
	}

	# Update user stats
	my $lastTopicTime = $topic->{lastReadTime} || 0;
	$m->dbDo("
		UPDATE users SET lastTopicId = ?, lastTopicTime = ? WHERE id = ?", 
		$topicId, $lastTopicTime, $userId);
}

# Update topic hit stats
$m->dbDo("
	UPDATE topics SET hitNum = hitNum + 1 WHERE id = ?", $topicId) 
	if $cfg->{topicHits} && !$m->{archive};

# Log action and finish
$m->logAction(2, 'topic', 'show', $userId, $boardId, $topicId);
$m->printFooter(undef, $boardId);
$m->finish();
