// Interface with the NewsPostController class via this object - NewsPosts.GetPost(123).MethodNameXYZ(567);
var NewsPosts = new NewsPostController();

function NewsPostController()
{
	// How long visual effects should take, in seconds
	this.EFFECTS_DURATION			= 0.4;

	// Max # of characters a comment (or response) can be
	this.MAX_COMMENT_CHARS			= 8192;
	this.MAX_RESPONSE_CHARS			= 8192;

	// The pages we hit for our AJAX stuff
	this.AJAX_COMMENT_SUBMIT_PAGE	= "/ajax/postblogcomment.php";
	this.AJAX_COMMENT_ADMIN_PAGE	= "/ajax/blogcommentadmin.php";

	// This private array will contain NewsPost instances for all posts on the page
	var posts = new Array();

	// This is the only way we interface with the class
	this.GetPost = function(post_id)
	{
		if(!(posts[post_id]))
		{
			posts[post_id] = new NewsPost(post_id);
		}

		return(posts[post_id]);
	}

	this.deleteCommentsByUserInOnePost = function (post_id, user_id) {
		var onSuccess = function () {
			window.location.href = window.location.href;
		}

		if (confirm("Are you sure you want to delete all comments by this user in this post? There's NO going back!")) {
			alert('Alright, please wait one moment.');

			var params = [];
			params['post_id'] = post_id;
			params['user_id'] = user_id;
			params['action'] = 'delete_by_user';

			var ajax = new AjaxRequest(NewsPosts.AJAX_COMMENT_ADMIN_PAGE);
			ajax.Send(params, onSuccess);
		}
	}


	// Inline class used to hand off things to the different objects for each user interaction
	function NewsPost(post_id)
	{
		// ================ PRIVATE variables ================
		var write_comment_window = new WriteCommentWindow(post_id);
		var share_this_window = new WindowDisplayer("sharecomment" + post_id, RenderHacks, "Don't share");
		var comments_box = new CommentsBox(post_id);
		var comments_box_unapproved = new UnapprovedCommentsBox(post_id);

		this.getID = function()
		{
			return(post_id);
		}

		this.getCommentWindow = function()
		{
			return(write_comment_window);
		}

		this.getShareWindow = function()
		{
			return(share_this_window);
		}

		// ================ PUBLIC functions ================
		this.ToggleCommentWindow = function()
		{
			share_this_window.IsOpen() ? share_this_window.Toggle(write_comment_window.Toggle) : write_comment_window.Toggle();
		}

		this.ToggleShareWindow = function()
		{
			write_comment_window.IsOpen() ? write_comment_window.Toggle(share_this_window.Toggle) : share_this_window.Toggle();
		}

		this.SaveComment = function()
		{
			// Pass .Save a callback to run once the comment is saved
			write_comment_window.Save(comments_box.AddComment);
		}

		this.Approve = function(ucomment_id)
		{
			// Pass along a callback for what to do upon a successful approve
			comments_box_unapproved.Approve(ucomment_id, comments_box.AddComment);
		}

		this.ApproveAll = function()
		{
			comments_box_unapproved.ApproveAll(comments_box.AddComment);
		}

		this.Delete = function(comment_id)
		{
			// Only proceed if they're REALLY SURE!
			if(!(confirm("Are you SURE you want to delete this comment?\n\n(This is permanent - there's no going back!)")))
			{
				return;
			}

			comments_box.Delete(comment_id);
		}

		this.DeleteUnapproved = function(ucomment_id)
		{
			if(!(confirm("Are you SURE you want to delete this comment?\n\n(This is permanent - there's no going back!)")))
			{
				return;
			}

			comments_box_unapproved.Delete(ucomment_id);
		}

		this.DeleteAllUnapproved = function()
		{
			if(confirm("Are you SURE you want to delete all of these unapproved comments?\n\n(This is permanent - there's no going back!)"))
			{
				comments_box_unapproved.DeleteAll();
			}
		}

		this.BanAuthor = function(comment_id, username)
		{
			if(confirm("Are you SURE you want to ban " + username + " from making comments on your news page?\n\n(You can always change these settings later.)"))
			{
				comments_box.BanAuthor(comment_id);
			}
		}

		this.BanAuthorUnapproved = function(ucomment_id, username)
		{
			if(confirm("Are you SURE you want to ban " + username + " from making comments on your news page?\n\n(You can always change these settings later.)"))
			{
				comments_box_unapproved.BanAuthor(ucomment_id);
			}
		}

		this.ToggleResponseWindow = function(comment_id)
		{
			comments_box.ToggleResponseWindow(comment_id);
		}

		this.EditResponse = function(comment_id)
		{
			comments_box.EditResponse(comment_id);
		}

		this.SaveResponse = function(comment_id)
		{
			comments_box.SaveResponse(comment_id);
		}

		this.DeleteResponse = function(comment_id)
		{
			if(confirm("Are you SURE you want to delete this response?\n\n(This is permanent - there's no going back!)"))
			{
				comments_box.DeleteResponse(comment_id);
			}
		}

		// ================ Below here are inline classes used by NewsPost only =================

		// This class represents the "write a new comment" window that opens on-demand
		function WriteCommentWindow(post_id)
		{
			// ================ PRIVATE variables ================
			var window_is_open = false;									// Is this window open or closed?
			var animation_in_progress = false;							// Is there an animation in progress or not?
			var comment_save_in_progress = false;						// Are we in mid-comment submit?
			var comment_needs_approval = false;							// Flag to indicate when we're dealing with an unapproved comment
			var comment_just_submitted = "";							// When they add a new comment, store the HTML of it here
			var comment_id_just_submitted = "";							// Goes with the above
			var save_handler;											// The function to call after a successful save

			// This controls the animation they see while a comment save is in progress
			var header_animator = new HeaderAnimator("postcomment" + post_id, "Saving Comment");

			// For sliding the "Post a comment" window up and down
			var window_displayer = new WindowDisplayer("postcomment" + post_id, RenderHacks);

			// ================ PUBLIC functions ================
			this.Toggle = function(callback)
			{
				// Only do something if we're not in the middle of something else
				if((!(animation_in_progress)) && (!(comment_save_in_progress)))
				{
					animation_in_progress = true;
					window_is_open ? CloseWindow(callback) : OpenWindow(callback);
				}
			}

			this.IsOpen = function()
			{
				return(window_is_open);
			}

			this.Save = function(onsave_callback_function)
			{
				if(!(comment_save_in_progress))
				{
					// First make sure they wrote a comment at all
					var comment_text = document.getElementById("postcomment" + post_id + "_text").value;
					if(comment_text == "")
					{
						alert("You can't submit a blank comment.");
						return;
					}

					// We're going to hit the server!  Mark this down.
					comment_save_in_progress = true;

					// Hang onto our save handler function.
					save_handler = onsave_callback_function;

					// Kick off our animation to show the user that we're doing stuff
					header_animator.Start();

					// Now send it to the server and see what happens.
					var params = new Array();
					params["post_id"] = post_id;
					params["comment"] = comment_text;

					var ajax = new AjaxRequest(NewsPosts.AJAX_COMMENT_SUBMIT_PAGE);
					ajax.Send(params, HandleCommentSave, CleanupAfterResponse);
				}
			}

			this.Open = function(callback)
			{
				OpenWindow(callback);
			}

			this.Close = function(callback)
			{
				CloseWindow(callback);
			}

			// ================ PRIVATE functions ================
			function OpenWindow(callback)
			{
				window_is_open = true;
				SetCommentLinkText();

				var our_callback = (typeof callback == "function" ? function() { FinishOpen(); callback(); } : FinishOpen);
				window_displayer.Open(our_callback);
			}

			function CloseWindow(callback)
			{
				window_is_open = false;
				SetCommentLinkText();

				var our_callback = (typeof callback == "function" ? function() { FinishClose(); callback(); } : FinishClose);
				window_displayer.Close(our_callback);
			}

			function FinishOpen()
			{
				animation_in_progress = false;
			}

			function FinishClose()
			{
				// Now that we've closed up, let's also reset the "in progress" animation
				header_animator.Stop();

				// One last thing - if this is a new comment save, we have more fun to do
				if((comment_needs_approval) || (comment_save_in_progress))
				{
					// Wipe out the stuff in the textarea
					ClearTextarea();

					if(comment_needs_approval)
					{
						// The previous alert() msg told the user what's going on - just exit
						comment_needs_approval = false;
					}
					else if(comment_save_in_progress)
					{
						// If this comment can be added immediately, use our passed-in handler to do it live
						save_handler(comment_just_submitted, comment_id_just_submitted);
					}
				}

				// Note that we're done!
				animation_in_progress = false;
				comment_save_in_progress = false;
			}

			function ClearTextarea()
			{
				// Blank out the actual textarea
				var comment_textarea = document.getElementById("postcomment" + post_id + "_text");
				comment_textarea.value = "";

				// And update the # of chars remaining
				CharactersRemaining(comment_textarea, NewsPosts.MAX_COMMENT_CHARS);
			}

			function SetCommentLinkText()
			{
				var linktext = (window_is_open ? "Don't comment" : "Leave a comment!");
				document.getElementById("postcomment" + post_id + "_link").firstChild.data = linktext;
			}

			function HandleCommentSave(response)
			{
				// Let's see if the author's approving comments for this one.
				if(response.GetField("unapproved") != null)
				{
					comment_needs_approval = true;

					var username = document.getElementById("page_username").firstChild.data;
					var msg = "Your comment has been submitted!\n\n";
					msg += "Once " + username + " approves it, your comment will be added to this news post.";
					alert(msg);

					// Kick off a close of the comment window
					CloseWindow();

					// When the animation finishes, it'll take care of everything else.  So we're done here.
					return;
				}

				// If we're on the post page, stay here; otherwise, redirect to post page.
				if(/\/news\/post/.test(window.location.href))		// This is the /news/post/XYZ page
				{
					// Grab the new comment and store it for later
					comment_just_submitted = response.GetField("comment_html");
					comment_id_just_submitted = response.GetField("cid");

					// Kick off a close of the comment window - code resumes running when it's done.
					CloseWindow();
				}
				else												// This is the /news/XYZ page, or the main page
				{
					// Let's take them directly to their shiny new comment
					window.location.href = response.GetField("url") + "#c" + response.GetField("cid");
				}
			}

			function CleanupAfterResponse()
			{
				header_animator.Stop();
				comment_save_in_progress = false;
			}
		}

		// This class represents the box containing ALL the comments for a post
		function CommentsBox(post_id)
		{
			var new_comment_html = "";
			var new_comment_id = null;
			var comments = new Array();
			var window_displayer = new WindowDisplayer("allcomments_approved", RenderHacks);

			this.AddComment = function(comment_html, comment_id, scroll_to_comment)
			{
				// By default, we'll scroll to the new comment and show it being added
				if(typeof scroll_to_comment == "undefined")
				{
					scroll_to_comment = true;
				}

				// Store this new comment
				new_comment_html = comment_html;
				new_comment_id = comment_id;

				// Bump up the number of comments
				SetNumComments(GetNumComments() + 1);

				// Also stash this new comment in our global arrays
				PageGlobals.comment_ids[PageGlobals.comment_ids.length] = new_comment_id;

				// NEED TO STORE THE TIMESTAMP HERE, TOO!

				// Now we add the visual elements
				if(GetNumComments() > 1)
				{
					AddHorizontalRule();
					scroll_to_comment ? ScrollToNewComment() : DisplayNewComment();
				}
				else
				{
					DisplayNewComment();
				}
			}

			this.Delete = function(comment_id)
			{
				// Show the user that stuff is happening
				var comment = GetComment(comment_id);
				comment.StartStatusAnimation("Deleting comment");

				var params = new Array();
				params["action"] = "deletecomment";
				params["post_id"] = post_id;
				params["comment_id"] = comment_id;

				var ajax = new AjaxRequest(NewsPosts.AJAX_COMMENT_ADMIN_PAGE);
				ajax.Send(params, HandleCommentDeleteResponse, comment.StopStatusAnimation);
			}

			this.BanAuthor = function(comment_id)
			{
				// Show the user that stuff is happening
				var comment = GetComment(comment_id);
				comment.StartStatusAnimation("Banning commenter");

				var params = new Array();
				params["action"] = "ban";
				params["post_id"] = post_id;
				params["comment_id"] = comment_id;

				// Let's build a closure to pass to do our AJAX response handling
				var handler = function(response)
				{
					HandleBanResponse(response);
					comment.StopStatusAnimation();
				}

				var ajax = new AjaxRequest(NewsPosts.AJAX_COMMENT_ADMIN_PAGE);
				ajax.Send(params, handler, comment.StopStatusAnimation);
			}

			this.ToggleResponseWindow = function(comment_id)
			{
				var win = GetComment(comment_id).GetResponseWindow();
				var textarea_id = "comment" + comment_id + "_response_text";

				win.IsOpen() ? StopCharsRemaining(textarea_id) : CheckCharsRemaining(textarea_id, NewsPosts.MAX_COMMENT_CHARS);
				win.Toggle();
			}

			this.EditResponse = function(comment_id)
			{
				var comment = GetComment(comment_id);
				var response_window = comment.GetResponseWindow();
				var textarea_id = "comment" + comment_id + "_response_text";

				// Order of what we do depends on whether it's an open or a close
				if(response_window.IsOpen())
				{
					StopCharsRemaining(textarea_id);
					response_window.Toggle(comment.ShowResponse);
				}
				else
				{
					CheckCharsRemaining(textarea_id, NewsPosts.MAX_COMMENT_CHARS);
					comment.HideResponse(response_window.Toggle);
				}
			}

			this.SaveResponse = function(comment_id)
			{
				// Pass this off to the appropriate comment
				GetComment(comment_id).SaveResponse();
			}

			this.DeleteResponse = function(comment_id)
			{
				var comment = GetComment(comment_id);
				comment.StartStatusAnimation("Deleting response");

				var params = new Array();
				params["action"] = "deleteresponse";
				params["post_id"] = post_id;
				params["comment_id"] = comment_id;

				var ajax = new AjaxRequest(NewsPosts.AJAX_COMMENT_ADMIN_PAGE);
				ajax.Send(params, HandleResponseDeleteResponse, comment.StopStatusAnimation);
			}

			function GetComment(comment_id)
			{
				if(!(comments[comment_id]))
				{
					comments[comment_id] = new Comment("comment" + comment_id);
				}

				return(comments[comment_id]);
			}

			function GetNumComments()
			{
				return(parseInt(document.getElementById("numcomments").firstChild.data.replace(/,/g, "")));
			}

			function SetNumComments(new_num_comments)
			{
				var comment_text = (new_num_comments != 1 ? "Comments" : "Comment");

				document.getElementById("numcommentstext").firstChild.data = comment_text;
				document.getElementById("numcomments").firstChild.data = FormatNumber(new_num_comments);
			}

			function AddHorizontalRule()
			{
				// Need to add a line across the bottom
				var element_hr = Builder.node('div', {className: 'hr', id: 'newline' + new_comment_id}, [
					Builder.node('hr')
				]);

				// Now we have something to scroll to (if we want to scroll)
				$('allcomments').appendChild(element_hr);
			}

			function ScrollToNewComment()
			{
				// Scroll down to the end, and once we're there, bring up the new comment
				new Effect.ScrollTo('newline' + new_comment_id, {afterFinish: DisplayNewComment});
			}

			function DisplayNewComment()
			{
				// Stick the new comment into a node, and append it
				var comment_div = DOMNodeFromHTML(new_comment_html);
				if(GetNumComments() == 1)
				{
					// If this is the only comment so far, we're going to show the box (as opposed to the comment)
					comment_div.style.display = "block";		// Normally starts out hidden
				}
				$('allcomments').appendChild(comment_div);

				// If we've got activity bars, let's change the class of the previous last comment, to get rid of the "last"
				if(GetNumComments() > 1)
				{
					var last_comment_id = PageGlobals.comment_ids[PageGlobals.comment_ids.length - 2];
					var last_comment_activity_bar = $("comment" + last_comment_id + "_activity");
					if(last_comment_activity_bar)
					{
						last_comment_activity_bar.className = "activity";
					}
				}

				// And let's give the new comment a "last", if it has an activity bar
				var last_activity_bar = document.getElementById("comment" + new_comment_id + "_activity");
				if(last_activity_bar)
				{
					last_activity_bar.className += " last";
				}

				// We're finally ready to show it!
				if(GetNumComments() > 1)
				{
					new Effect.Appear("comment" + new_comment_id, {duration: NewsPosts.EFFECTS_DURATION, afterFinish: PerformRenderHack});
				}
				else
				{
					window_displayer.Open(PerformRenderHack);
				}
			}

			function HandleCommentDeleteResponse(response)
			{
				// Comment was deleted by the server - remove it from the box.
				RemoveComment(response.GetField("cid"));
			}

			function HandleResponseDeleteResponse(response)
			{
				// Response got deleted - hide it
				var comment = GetComment(response.GetField("cid"));
				var response_window = comment.GetResponseWindow();

				// Slap in our new activity bar
				var callback = function()
				{
					comment.StopStatusAnimation();
					comment.SetActivityBar(response.GetField("activity"));
					comment.GetResponseWindow().Reset(true);
				}

				// If the "Edit" window is open, then we just close it - otherwise we hide the response
				response_window.IsOpen() ? response_window.Toggle(callback) : comment.HideResponse(callback);
			}

			function HandleBanResponse(response)
			{
				// Just let the user know that the ban was successful.
				alert("OK - " + response.GetField("user") + " has been banned from commenting on your News page, effective immediately.");
			}

			function RemoveComment(comment_id)
			{
				var comment = new Comment("comment" + comment_id);

				// First change our comment totals.
				SetNumComments(GetNumComments() - 1);

				// Let's check if this is the last comment.  If it is, then the whole box goes away.
				if(GetNumComments() > 0)
				{
					// Is this comment the last one?  If so, we need to juggle some classes around.
					if(comment_id == PageGlobals.comment_ids[PageGlobals.comment_ids.length - 1])
					{
						// It's the last one, so change the class of the preceding comment
						var activity_div = $("comment" + PageGlobals.comment_ids[PageGlobals.comment_ids.length - 2] + "_activity");
						activity_div.className += " last";
					}

					// Now wipe it out!
					comment.Delete();
				}
				else		// Only one comment left - hide the whole box, and kill the comment when done
				{
					var callback = function()
					{
						comment.Delete();
						document.getElementById("allcomments_approved_wrapper").style.height = null;
					}

					window_displayer.Close(callback);
				}
			}

			// Sigh... thank you, browser makers, for forcing me to do this bit of inanity
			function PerformRenderHack()
			{
				var paragraph_element = document.getElementById("comment" + new_comment_id).getElementsByTagName("p")[0];
				AddRemoveSpace(paragraph_element);
			}
		}

		// This is only used when a post is approve-only for comments
		function UnapprovedCommentsBox(post_id)
		{
			var approve_handler = null;
			var header_animator = null;
			var comments = new Array();

			this.Approve = function(ucomment_id, onapprove_callback_function)
			{
				approve_handler = onapprove_callback_function;

				// Show the user that stuff is happening
				var comment = GetComment(ucomment_id);
				comment.StartStatusAnimation("Approving comment");

				var params = new Array();
				params["action"] = "approve";
				params["post_id"] = post_id;
				params["ucomment_id"] = ucomment_id;

				var ajax = new AjaxRequest(NewsPosts.AJAX_COMMENT_ADMIN_PAGE);
				ajax.Send(params, HandleApproveResponse, comment.StopStatusAnimation);
			}

			this.ApproveAll = function(onapprove_callback_function)
			{
				approve_handler = onapprove_callback_function;

				// Show the user that stuff is happening
				GetHeaderAnimator().Start("Approving All");

				var params = new Array();
				params["action"] = "approve_all";
				params["post_id"] = post_id;

				var ajax = new AjaxRequest(NewsPosts.AJAX_COMMENT_ADMIN_PAGE);
				ajax.Send(params, HandleApproveAllResponse, CleanupHeaderAnimation);
			}

			this.Delete = function(ucomment_id)
			{
				// Show the user that stuff is happening
				var comment = GetComment(ucomment_id);
				comment.StartStatusAnimation("Deleting comment");

				var params = new Array();
				params["action"] = "delete_unapproved";
				params["post_id"] = post_id;
				params["ucomment_id"] = ucomment_id;

				var ajax = new AjaxRequest(NewsPosts.AJAX_COMMENT_ADMIN_PAGE);
				ajax.Send(params, HandleCommentDeleteResponse, comment.StopStatusAnimation);
			}

			this.DeleteAll = function()
			{
				// Show the user that stuff is happening
				GetHeaderAnimator().Start("Deleting All");

				var params = new Array();
				params["action"] = "delete_all_unapproved";
				params["post_id"] = post_id;

				var ajax = new AjaxRequest(NewsPosts.AJAX_COMMENT_ADMIN_PAGE);
				ajax.Send(params, HandleDeleteAllResponse, CleanupHeaderAnimation);
			}

			this.BanAuthor = function(ucomment_id)
			{
				// Show the user that stuff is happening
				var comment = GetComment(ucomment_id);
				comment.StartStatusAnimation("Banning commenter");

				var params = new Array();
				params["action"] = "ban";
				params["post_id"] = post_id;
				params["ucomment_id"] = ucomment_id;

				// Let's build a closure to pass to do our AJAX response handling
				var handler = function(response)
				{
					HandleBanResponse(response);
					comment.StopStatusAnimation();
				}

				var ajax = new AjaxRequest(NewsPosts.AJAX_COMMENT_ADMIN_PAGE);
				ajax.Send(params, handler, comment.StopStatusAnimation);
			}

			function GetHeaderAnimator()
			{
				if(header_animator == null)
				{
					header_animator = new HeaderAnimator("unapproved");
				}

				return(header_animator);
			}

			function GetNumComments()
			{
				return(parseInt(document.getElementById("numcomments_unapproved").firstChild.data.replace(/,/g, "")));
			}

			function SetNumComments(new_num_comments)
			{
				document.getElementById("numcomments_unapproved").firstChild.data = FormatNumber(new_num_comments);
			}

			function CleanupHeaderAnimation()
			{
				GetHeaderAnimator().Stop();
			}

			function HandleApproveResponse(response)
			{
				ApproveComments(new Array(response.GetField("ucid")), new Array(response.GetField("cid")), new Array(response.GetField("comment_html")));
			}

			function HandleApproveAllResponse(response)
			{
				// Let's build our list of comments to deal with
				var comments = response.GetNodes("comment");
				var ids = new Array();
				var uids = new Array();
				var html_list = new Array();

				for(var i=0; i<comments.length; i++)
				{
					ids[ids.length] = comments[i].getElementsByTagName("cid")[0].firstChild.data;
					uids[uids.length] = comments[i].getElementsByTagName("ucid")[0].firstChild.data;
					html_list[html_list.length] = comments[i].getElementsByTagName("comment_html")[0].firstChild.data;
				}

				ApproveComments(uids, ids, html_list);
			}

			function HandleCommentDeleteResponse(response)
			{
				// Just remove this comment from the box, and nothing else.
				RemoveComment(response.GetField("ucid"));
			}

			function HandleDeleteAllResponse(response)
			{
				// All the comments were deleted - hooray.  Just hide the "Awaiting Approval" box and we're done.
				HideEntireBox();
			}

			function HandleBanResponse(response)
			{
				// Just let the user know that the ban was successful.
				alert("OK - " + response.GetField("user") + " has been banned from commenting on your News page, effective immediately.");
			}

			function ApproveComments(ucomment_id_list, comment_id_list, comment_html_list)
			{
				for(var i=0; i<ucomment_id_list.length; i++)
				{
					// First remove the comment from this box
					RemoveComment(ucomment_id_list[i]);

					// Next, call the appropriate approve handler
					approve_handler(comment_html_list[i], comment_id_list[i], false);
				}
			}

			function GetComment(ucomment_id)
			{
				if(!(comments[ucomment_id]))
				{
					comments[ucomment_id] = new Comment("ucomment" + ucomment_id);
				}

				return(comments[ucomment_id]);
			}

			function RemoveComment(ucomment_id)
			{
				// Let's check if this is the last comment.  If it is, then the whole box goes away.
				if(GetNumComments() > 1)
				{
					// First change our comment totals.
					SetNumComments(GetNumComments() - 1);

					// Is this comment the last one?  If so, we need to juggle some classes around.
					if(ucomment_id == PageGlobals.ucomment_ids[PageGlobals.ucomment_ids.length - 1])
					{
						// It's the last one, so change the class of the preceding comment
						var activity_div = $("ucomment" + PageGlobals.ucomment_ids[PageGlobals.ucomment_ids.length - 2] + "_activity");
						activity_div.className += " last";
					}

					// Eliminate the comment from our "unapproved" list
					var comment = GetComment(ucomment_id);
					comment.Delete();
				}
				else		// Only one comment left - we know what to do...
				{
					HideEntireBox();
				}
			}

			function HideEntireBox()
			{
				var div_wrapper = document.getElementById("unapproved_container");
				var div_wrapper_inner = document.getElementById("unapproved_container_inner");

				// We have to give the outer wrapper an explicit height, so it stays tall after the inner stuff has faded
				div_wrapper.style.height = div_wrapper.offsetHeight + "px";

				// OK, we hardened our height, now we can fade out
				new Effect.Fade(div_wrapper_inner, {duration: NewsPosts.EFFECTS_DURATION, afterFinish: SlideUp});
			}

			function SlideUp()
			{
				new Effect.SlideUp("unapproved_container", {duration: NewsPosts.EFFECTS_DURATION});
			}
		}

		// This class represents a comment - can be approved or unapproved
		function Comment(element_id)
		{
			var comment_id = (element_id.match(/^u?comment([0-9]+)$/))[1];
			var is_unapproved = /^ucomment/.test(element_id);
			var comment_displayer = new WindowDisplayer(element_id, RenderHacks);
			var response_window = null;
			var response_displayer = new WindowDisplayer(element_id + "_response_body", RenderHacks);
			var status_animator = null;

			this.GetCommentID = function()
			{
				return(comment_id);
			}

			this.Delete = function()
			{
				// We have to give the outer wrapper an explicit height, so it stays tall after the inner stuff has faded
				var div_wrapper = document.getElementById(element_id + "_wrapper");
				if(div_wrapper.offsetHeight > 0)
				{
					div_wrapper.style.height = (div_wrapper.offsetHeight + 5) + "px";

					// Now get rid of it.
					comment_displayer.Close(RemoveHTML);
				}
				else		// In this case, our entire box was hidden - just remove the HTML and go away
				{
					RemoveHTML();
				}
			}

			this.GetNext = function()
			{
				// Search our global array for this guy
				for(var i=0; i<PageGlobals.comment_ids.length-1; i++)
				{
					if(PageGlobals.comment_ids[i] == comment_id)
					{
						return(new Comment("comment" + PageGlobals.comment_ids[i+1]));
					}
				}

				// If we're here, we must be the last comment
				return(null);
			}

			this.GetResponseWindow = function()
			{
				if(response_window == null)
				{
					response_window = new CommentResponseWindow(element_id + "_response", this);
				}

				return(response_window);
			}

			this.HasResponse = function()
			{
				return(document.getElementById(element_id + "_response_body_wrapper_inner") != null);
			}

			this.SaveResponse = function()
			{
				this.GetResponseWindow().Save();
			}

			this.SetResponse = function(response_html)
			{
				// Convert our HTML to DOM nodes, and add some style stuff
				var response = DOMNodeFromHTML(response_html);
				response.style.display = "none";
				var response_wrapper_div = response.getElementsByTagName("div")[0];
				response_wrapper_div.style.visibility = "hidden";

				// Do we already have a response here?  If so, swap it in, otherwise add a new one.
				var response_element = document.getElementById(element_id + "_response_body");
				if(response_element)
				{
					response_element.parentNode.replaceChild(response, response_element);
				}
				else
				{
					var comment_div = document.getElementById(element_id + "_bundle");
					comment_div.parentNode.insertBefore(response, comment_div.nextSibling);
				}
			}

			this.ShowResponse = function(callback)
			{
				// Slap some extra code on the back of whatever callback they might pass along
				var afteropen_callback = function()
				{
					if(typeof callback == "function")
					{
						callback();
					}

					// We need this because IE (weak) has problems with making things too big
					document.getElementById(element_id + "_response_body_wrapper").style.height = document.getElementById(element_id + "_response_body_wrapper_inner").offsetHeight + "px";
				};
				response_displayer.Open(afteropen_callback);
			}

			this.HideResponse = function(callback)
			{
				// Let's make sure we've "hardened" the height on this - our slide/fade effect needs it
				document.getElementById(element_id + "_response_body_wrapper").style.height = document.getElementById(element_id + "_response_body_wrapper_inner").offsetHeight + "px";

				// Now we can do the close.
				response_displayer.Close(callback);
			}

			this.SetActivityBar = function(activity_bar_html)
			{
				// Stick in the HTML for the new activity bar
				document.getElementById(element_id + "_activity").innerHTML = activity_bar_html;
			}

			this.StartStatusAnimation = function(animation_text)
			{
				GetStatusAnimator().Start(animation_text);
			}

			this.StopStatusAnimation = function()
			{
				GetStatusAnimator().Stop();
			}

			function RemoveHTML()
			{
				// Ditch the appropriate <hr>
				HideHorizontalRule();

				// Finally, wipe out this comment in the DOM, so it doesn't get found later by "Approve All"
				comment_node = document.getElementById(element_id);
				comment_node.parentNode.removeChild(comment_node);

				// Last thing - remove it from our global array.
				var id_array = is_unapproved ? PageGlobals.ucomment_ids : PageGlobals.comment_ids;
				for(var i=0; i<id_array.length; i++)
				{
					if(id_array[i] == comment_id)
					{
						id_array.splice(i, 1);
						break;
					}
				}
			}

			function HideHorizontalRule()
			{
				var element = document.getElementById(element_id);

				// Now traverse the list of sibling nodes, trying to find an <hr>
				var sibling = element.nextSibling;
				while((sibling != null) && (sibling.className != "hr"))
				{
					sibling = sibling.nextSibling;
				}

				// If sibling is STILL null, we need to look in the the other direction
				if(sibling == null)
				{
					sibling = element.previousSibling;
					while((sibling != null) && (sibling.className != "hr"))
					{
						sibling = sibling.previousSibling;
					}
				}

				// sibling will only be null here if there are no <hr>s left
				if(sibling != null)
				{
					sibling.parentNode.removeChild(sibling);
				}
			}

			function GetStatusAnimator()
			{
				if(status_animator == null)
				{
					status_animator = new DotAnimatedText(element_id + "_activity", "modworking");
				}

				return(status_animator);
			}

			function CommentResponseWindow(element_id, comment)
			{
				var window_is_open = false;
				var animation_in_progress = false;
				var window_displayer = new WindowDisplayer(element_id, RenderHacks);
				var original_link_text = GetResponseLinkText();
				var toggle_callback = null;
				var original_activity_class = null;
				var header_animator = new HeaderAnimator(element_id, "Saving Response");

				this.Toggle = function(onfinish_callback)
				{
					if(!(animation_in_progress))
					{
						animation_in_progress = true;

						toggle_callback = null;
						if(typeof onfinish_callback == "function")
						{
							toggle_callback = onfinish_callback;
						}

						window_is_open ? CloseWindow() : OpenWindow();
					}
				}

				this.IsOpen = function()
				{
					return(window_is_open);
				}

				this.Reset = function(clear_textarea)
				{
					// Make sure the "delete response" etc text is correct
					original_link_text = GetResponseLinkText();

					// Let's wipe out any text that may be lingering in the textarea
					if(clear_textarea == true)
					{
						var textarea_element = document.getElementById(element_id + "_text");
						textarea_element.value = "";

						// This will reset the # of chars remaining
						CharactersRemaining(textarea_element, NewsPosts.MAX_RESPONSE_CHARS);
					}
				}

				this.Save = function()
				{
					// First make sure they wrote a response at all
					var response_text = document.getElementById("comment" + comment_id + "_response_text").value;
					if(response_text == "")
					{
						alert("You can't submit a blank comment.");
						return;
					}

					// Show the user that stuff is happening
					header_animator.Start();

					var params = new Array();
					params["action"] = "response";
					params["post_id"] = post_id;
					params["comment_id"] = comment_id;
					params["response"] = response_text;

					var ajax = new AjaxRequest(NewsPosts.AJAX_COMMENT_ADMIN_PAGE);
					ajax.Send(params, HandleResponseSaveResponse, FinishErrorCleanup);
				}

				function HandleResponseSaveResponse(response)
				{
					// Store our new response HTML in our comment
					comment.SetResponse(response.GetField("response"));

					// Apply the new activity bar
					comment.SetActivityBar(response.GetField("activity"));
					comment.GetResponseWindow().Reset();

					// Now we close the "Edit" tray, and pass in our function to show the response when it's done
					StopCharsRemaining("comment" + response.GetField("cid") + "_response_text");
					comment.GetResponseWindow().Toggle(comment.ShowResponse);
				}

				function OpenWindow()
				{
					window_is_open = true;

					SetResponseLinkText();
					FixActivityBar();
					window_displayer.Open(FinishSuccessCleanup);
				}

				function CloseWindow()
				{
					window_is_open = false;

					SetResponseLinkText();
					FixActivityBar();
					window_displayer.Close(FinishSuccessCleanup);
				}

				function GetResponseLinkText()
				{
					var link_element = document.getElementById(element_id + "_link");
					var text = "";

					if(link_element)
					{
						text = link_element.firstChild.data;
					}

					return(text);
				}

				function SetResponseLinkText()
				{
					var link_element = document.getElementById(element_id + "_link");
					if(link_element)
					{
						link_element.firstChild.data = (window_is_open ? "\u2193 close response window \u2193" : original_link_text);
					}
				}

				// This function takes care of the "last" class on the last comment, if necessary
				function FixActivityBar()
				{
					var activity_bar = $("comment" + (element_id.match(/^comment([0-9]+)/))[1] + "_activity");
					if(window_is_open)
					{
						original_activity_class = activity_bar.className;
						activity_bar.className = "activity";
					}
					else
					{
						activity_bar.className = original_activity_class;
					}
				}

				function FinishErrorCleanup()
				{
					animation_in_progress = false;
					header_animator.Stop();
				}

				function FinishSuccessCleanup()
				{
					FinishErrorCleanup();

					// We might need to run something upon finish
					if(toggle_callback != null)
					{
						toggle_callback();
					}
				}
			}
		}

		function RenderHacks(id)
		{
			// Does this box have a textarea?
			var textarea_element = document.getElementById(id + "_text");
			if((textarea_element) && (textarea_element.offsetHeight > 0))
			{
				// Let's add a character and take it away - to force a redraw for some lame browsers
				AddRemoveSpace(textarea_element);

				// Put the keyboard focus on the textarea that's just opened
				textarea_element.focus();
			}

			// Does this box have a header?
			var header_text_element = document.getElementById(id + "_header");
			if((header_text_element) && (header_text_element.offsetHeight > 0))
			{
				// Safari has a bug whereby we have to force it to re-render the content that's slid down
				AddRemoveSpace(header_text_element);
			}

			// If this is a response, we also need to make Safari re-render it - sigh (Safari kills sometimes)
			var response_element = document.getElementById(id + "_response_body");
			if((response_element) && (response_element.offsetHeight > 0))
			{
				AddRemoveSpace(response_element.getElementsByTagName("p")[0]);
			}

			// IE also likes to muck up responses
			if(/_response_body$/.test(id))
			{
				AddRemoveSpace(document.getElementById(id).getElementsByTagName("p")[0]);
			}

			// Look for an activity bar
			var comment_id = GetCommentID();
			if(comment_id != null)
			{
				// Let's see if there's a comment below us.  If there is, we need an ugly hack to force a re-render.
				var this_comment = new Comment("comment" + comment_id);
				var next_comment = this_comment.GetNext();

				if(next_comment != null)
				{
					var comment_element = document.getElementById("comment" + next_comment.GetCommentID());
					if((comment_element) && (comment_element.offsetHeight > 0))
					{
						// OK, we've got a comment below us.
						AddRemoveSpace(document.getElementById("comment" + next_comment.GetCommentID()).getElementsByTagName("p")[0]);
					}
				}
			}

			function GetCommentID()
			{
				var matches = id.match(/^comment([0-9]+)/);
				return(((matches != null) && (matches.length > 0)) ? matches[1] : null);
			}
		}
	}
}
