/// <reference path="third-party/jquery-1.3.2-vsdoc2.js"/>

// Handles all client-side voting functionality
var vote = function() {

    var voteTypeIds = {
        informModerator: -1, // not submitted to votes, but to messages controller
        undoMod: 0,
        acceptedByOwner: 1,
        upMod: 2,
        downMod: 3,
        offensive: 4,
        favorite: 5,
        close: 6,
        reopen: 7,
        deletion: 10,
        undeletion: 11,
        spam: 12
    };

    var imgDownOff = imagePath.replace("{0}", "vote-arrow-down.png");
    var imgDownOn = imagePath.replace("{0}", "vote-arrow-down-on.png");
    var imgUpOff = imagePath.replace("{0}", "vote-arrow-up.png");
    var imgUpOn = imagePath.replace("{0}", "vote-arrow-up-on.png");
    var imgFavOn = imagePath.replace("{0}", "vote-favorite-on.png");
    var imgFavOff = imagePath.replace("{0}", "vote-favorite-off.png");

    var bindAnonymousDisclaimers = function() {
        var anchor = '<a href="/users/login?returnurl=' + escape(document.location) + '">login or register</a>';

        var nodes = $("div.vote").find("img").not(".vote-accepted");

        if (typeof allowUpVote != "undefined" && allowUpVote) {
            nodes = nodes.not(".vote-up");
        }

        // Clicking on a voting arrow will show a message to login/register..
        nodes.unbind("click").click(function(event) {
            showNotification($(event.target), 'Please ' + anchor + ' to use voting.');
        });

        getFlagLinks().unbind("click").click(function(event) {
            showNotification($(event.target), "Please " + anchor + " to flag this post.");
        });
    };

    var bindVoteClicks = function(jDivVote) {

        if (!jDivVote)
            jDivVote = "div.vote";

        // bind click events to our images..
        $(jDivVote).find("img.vote-up").unbind("click").click(function(event) {
            vote.up($(event.target));
        });
        $(jDivVote).find("img.vote-down").unbind("click").click(function(event) {
            vote.down($(event.target));
        });
        $(jDivVote).find("img.vote-favorite").unbind("click").click(function(event) {
            vote.favorite($(event.target));
        });
    };

    var unbindVoteClicks = function(jClicked) {
        jClicked.parent().find("img").not(".vote-accepted").unbind("click");
    };
    
    var fetchVotesCast = function(questionId) {
        $.ajax({
            type: "GET",
            url: "/posts/" + questionId + "/votes",
            dataType: "json",
            success: highlightExistingVotes,
            cache: false // IE will cache ajax calls - don't allow it..
        });
    };

    var highlightExistingVotes = function(jsonArray) {
        $.each(jsonArray, function() {
            var jDiv = $("div.vote:has(input[value=" + this.PostId + "])");

            switch (this.VoteTypeId) {
                case voteTypeIds.upMod:
                    jDiv.find("img.vote-up").attr("src", imgUpOn);
                    break;

                case voteTypeIds.downMod:
                    jDiv.find("img.vote-down").attr("src", imgDownOn);
                    break;

                case voteTypeIds.favorite:
                    jDiv.find("img.vote-favorite").attr("src", imgFavOn);
                    jDiv.find("div.favoritecount b").addClass("favoritecount-selected");
                    break;

                default:
                    alert("site.vote.js > highlightExistingVotes has no case for " + this.VoteTypeId);
                    break;
            }
        });
        
        // next time vote.init is called, be sure to fetch the latest votes
        votesCast = null;
    };

    var getAcceptedAnswerLinks = function() {
        return $("div.vote img[id^='vote-accepted-']");
    };

    var getLockPostLinks = function() {
        return $("div.post-menu a[id^='lock-post-']");
    };

    var getFlagLinks = function() {
        return $("div.post-menu a[id^='flag-post-']");
    };

    var preloadImages = function() {
        var img = new Image();
        img.src = imgUpOn;

        img = new Image();
        img.src = imgDownOn;
    };

    var isUpSelected = function(jUp) {
        return jUp.attr("src") == imgUpOn;
    };

    var isFavoriteSelected = function(jFavorite) {
        return jFavorite.attr("src") == imgFavOn;
    };
    var isDownSelected = function(jDown) {
        return jDown.attr("src") == imgDownOn;
    };

    var getPostId = function(jClicked) {
        return jClicked.parent().find("input").val();
    };

    var reset = function(jUp, jDown) {
        if (isUpSelected(jUp)) {
            jUp.attr("src", imgUpOff);
        }

        if (isDownSelected(jDown)) {
            jDown.attr("src", imgDownOff);
        }
    };

    var updateModScore = function(jClicked, incrementAmount) {
        var jScore = jClicked.siblings("span.vote-count-post");
        jScore.text(parseInt(jScore.text(), 10) + incrementAmount);
    }

    var submitModVote = function(jClicked, voteTypeId) {
        unbindVoteClicks(jClicked); // disable voting during a vote..

        var postId = getPostId(jClicked);
        submit(jClicked, postId, voteTypeId, modVoteResult);
    };

    var submit = function(jClicked, postId, voteTypeId, callback, optionalFormData) {
        var formData = { 'fkey': fkey }; // fkey is found in Show.aspx's head..

        // merge call-specific form data..
        if (optionalFormData)
            for (var name in optionalFormData)
                formData[name] = optionalFormData[name];
        
        $.ajax({
            type: 'POST',
            url: '/posts/' + postId + '/vote/' + voteTypeId,
            data: formData,
            dataType: 'json',
            success: function(data) { callback(jClicked, postId, data); },
            error: function() { showNotification(jClicked, 'An error has occurred - please retry your request.'); }
        });
    };

    var modVoteResult = function(jClicked, postId, data) {
        if (data.Success) {
            if (data.Message)
                showFadingNotification(jClicked, data.Message);
        }
        else if (window.console && window.console.firebug && (!data.Message || data.Message.length < 5)) {
            showNotification(jClicked, "FireBug seems to be enabled, which can sometimes interfere with voting;<br>" + 
                                       "please refresh the page to see if your vote was processed.<br><br>" + 
                                       "If this persists, consider disabling FireBug for this site.");
        }
        else {
            showNotification(jClicked, data.Message);
            
            // unhighlight vote arrows
            reset(jClicked, jClicked);
            
            // Undo score change..
            jClicked.parent().find("span.vote-count-post").text(data.NewScore);

            if (data.LastVoteTypeId) {
                selectPreviousVote(jClicked, data.LastVoteTypeId);
            }
        }
        bindVoteClicks(jClicked.parent()); // re-enable voting..
    };

    var selectPreviousVote = function(jClicked, voteTypeId) {
        var img, imgSelected;
        if (voteTypeId == voteTypeIds.upMod) {
            img = "img.vote-up";
            imgSelected = imgUpOn;
        }
        else if (voteTypeId == voteTypeIds.downMod) {
            img = "img.vote-down";
            imgSelected = imgDownOn;
        }

        if (img)
            jClicked.parent().find(img).attr("src", imgSelected);
    };

    var showNotification = function(jClicked, msg) {
        var div = $('<div class="vote-notification"><h2>' + msg + '</h2>(click on this box to dismiss)</div>');

        div.click(function(event) {
            $(".vote-notification").fadeOut("fast", function() { $(this).remove(); });
        });

        jClicked.parent().append(div);
        div.fadeIn("fast");
    };

    var showFadingNotification = function(jClicked, msg) {
        var div = $('<div class="vote-notification"><h2>' + msg + '</h2></div>');

        jClicked.parent().append(div);
        div.fadeIn("fast");

        var fadeOut = function() {
            $(".vote-notification").fadeOut("fast", function() { $(this).remove(); });
        };

        var duration = Math.max(2500, msg.length * 40); // longer messages should stick around..
        setTimeout(fadeOut, duration);
    };

    // Public methods on vote
    return {

        init: function(questionId) {
            if (typeof allowUpVote != "undefined" && allowUpVote) { // allowUpVote is on Show.aspx..
                preloadImages();

                // votesCast is defined in questions/show head
                if (votesCast == null)
                    fetchVotesCast(questionId);
                else
                    highlightExistingVotes(votesCast);

                bindVoteClicks();

                getFlagLinks().unbind("click").click(function(event) {
                    vote.flag($(event.target));
                });

                if (typeof isRegistered == "undefined" || !isRegistered) {
                    bindAnonymousDisclaimers();
                }
            }            
            else bindAnonymousDisclaimers();

            // Always bind "Is Answer" ability..
            getAcceptedAnswerLinks().unbind("click").click(function(event) {
                vote.acceptedAnswer($(event.target));
            });

            var jCloseLink = $("div.post-menu a[id^='close-question-']");
            jCloseLink.unbind("click").click(function(event) {
                vote.close(jCloseLink);
            });

            $("div.post-menu a[id^='delete-question-']").unbind("click").click(function(event) {
                vote.deletion($(event.target));
            });
        },


        up: function(jClicked) {

            var jUp = jClicked.parent().find("img.vote-up");
            var jDown = jClicked.parent().find("img.vote-down");

            var isSelected = isUpSelected(jUp);
            var isReversal = isDownSelected(jDown);
            var incrementAmount = isSelected ? -1 : (isReversal ? 2 : 1);

            updateModScore(jClicked, incrementAmount);
            reset(jUp, jDown);

            if (!isSelected) { // now select it..
                jUp.attr("src", imgUpOn);
            }

            submitModVote(jClicked, isSelected ? voteTypeIds.undoMod : voteTypeIds.upMod);
        },

        down: function(jClicked) {

            var jUp = jClicked.parent().find("img.vote-up");
            var jDown = jClicked.parent().find("img.vote-down");

            var isSelected = isDownSelected(jDown);
            var isReversal = isUpSelected(jUp);
            var incrementAmount = isSelected ? 1 : (isReversal ? -2 : -1);

            updateModScore(jClicked, incrementAmount);
            reset(jUp, jDown);

            if (!isSelected) { // now select it..
                jDown.attr("src", imgDownOn);
            }

            submitModVote(jClicked, isSelected ? voteTypeIds.undoMod : voteTypeIds.downMod);
        },


        favorite: function(jClicked) {
            // TODO: implement callback error messages..
            var jFavoriteCount = jClicked.parent().find("div.favoritecount b");
            var count = parseInt("0" + jFavoriteCount.text().replace(/^\s+|\s+$/g, ""), 10);

            if (!isFavoriteSelected(jClicked)) {
                jClicked.attr("src", imgFavOn);
                jFavoriteCount.addClass("favoritecount-selected").text(++count);
            } else {
                jClicked.attr("src", imgFavOff);
                jFavoriteCount.removeClass("favoritecount-selected").text((count-- <= 0) ? "" : count);
            }

            // disallow favorite clicking during submission..
            jClicked.unbind("click");

            submit(jClicked, getPostId(jClicked), voteTypeIds.favorite, function(data) {
                // rebind once we come back (if evar! omg!)..
                jClicked.click(function(event) {
                    vote.favorite($(event.target));
                });
            });
        },


        acceptedAnswer: function(jClicked) {
        
            // hasOpenBounty is defined in Questions/Show's head
            if (typeof hasOpenBounty != "undefined" && hasOpenBounty) {
                var msg = ($("#" + postId + "-is-owned-by-question-owner").length > 0) ?
                    "Are you sure you want to accept your own answer?  You will STILL LOSE the offered rep and THIS CANNOT BE UNDONE!" :
                    "Are you sure you want to accept this answer to your bounty question - THIS CANNOT BE UNDONE!";

                if (!confirm(msg))
                    return;
            }
            
            // isBountyQuestion is defined in Questions/Show's head
            if (typeof isBountyQuestion != "undefined" && isBountyQuestion && typeof hasOpenBounty == "undefined") {
                showNotification(jClicked, "You cannot change the accepted answer on a bounty question");
                return;
            }

            // prevent other clicks
            getAcceptedAnswerLinks().unbind("click");

            var postId = jClicked.attr("id").substring("vote-accepted-".length);

            submit(jClicked, postId, voteTypeIds.acceptedByOwner, function(jClicked, postId, data) {
                
                if (data.Message.search(/vote-accepted/) !== -1) { // a successful (un)accept answer vote passes back the new check mark image to display
                    var commentsLinkClass = "comments-link";
                    var commentsContainerClass = "comments-container";

                    // remove old styles
                    $("div.answer").removeClass("accepted-answer");
                    $("img.vote-accepted").attr("src", imagePath.replace("{0}", "vote-accepted.png"));
                    $("a.comments-link-accepted").removeClass().addClass(commentsLinkClass);
                    $("div.comments-container-accepted").removeClass().addClass(commentsContainerClass);

                    // block that will change this post back to its owner colors
                    var resetOwnerStyles = function(jAnswerDiv) {
                        jAnswerDiv.find(".comments-link").removeClass().addClass("comments-link-owner")
                            .end() // move back to jAnswerDiv
                            .find(".comments-container").removeClass().addClass("comments-container-owner");
                    };

                    // if we removed an accepted from the question owner's answer, ensure it's back to its blue
                    $("div.answer:has(input[id$='-is-owned-by-question-owner'])").not(".owner-answer").addClass("owner-answer")
                        .each(function() { resetOwnerStyles($(this)) });

                    if (data.Message.search(/vote-accepted-on.png/) > -1) { // we accepted an answer
                        $("div.answer:has(img[id^='vote-accepted-" + postId + "'])").removeClass("owner-answer").addClass("accepted-answer");
                        commentsLinkClass = commentsLinkClass + "-accepted";
                        commentsContainerClass = commentsContainerClass + "-accepted";
                    }
                    else if ($("#" + postId + "-is-owned-by-question-owner").length > 0) { // toggled own accepted
                        resetOwnerStyles($("#answer-" + postId));
                    }
                    // don't have to worry about unaccepting - the remove old styles code above has reset for us

                    $("a[id='comments-link-" + postId + "']").removeClass().addClass(commentsLinkClass)
                        .siblings("div").removeClass().addClass(commentsContainerClass);

                    jClicked.attr("src", data.Message);

                    if (typeof hasOpenBounty != "undefined" && hasOpenBounty) { // we just accepted an answer on our bounty question..
                        hasOpenBounty = false;
                        $("#bounty-notification").hide();
                    }
                }
                else { // the server sent back a reason that it blocked this vote - inform the user
                    showNotification(jClicked, data.Message);
                }
                
                // Rebind clicks
                getAcceptedAnswerLinks().click(function(event) {
                    vote.acceptedAnswer($(event.target));
                });
            });
        },

        flag: function(jClicked) {
            var postId = jClicked.attr("id").substring("flag-post-".length);
            var formId = "form-flag-" + postId;
            
            // hidden input defined in both question and answer sections
            // post owners may only flag "inform moderator" on their posts
            var isPostOwnedByCurrentUser = $('#' + postId + '-is-owned-by-current-user').length > 0;
            
            // array contains a votetype to check against, text to show, and if it should be visible to the user
            // inform moderator no longer is a vote, but a submission to the messages controller
            var votes = [
                            [voteTypeIds.offensive, "Offensive, Abusive, or Hate Speech", !isPostOwnedByCurrentUser],
                            [voteTypeIds.spam, "Spam", !isPostOwnedByCurrentUser],
                            [voteTypeIds.informModerator, "Requires Moderator attention", true]
                        ];
            
            var html = '<div class="vote-notification flag-menu"><h2>Please flag with care:</h2>';
            html += '<div class="flag-reasons"><form id="' + formId + '">';
            
            for (var i = 0; i < votes.length; i++) {
                if (votes[i][2]) {
                    var radioId = "flag-radio" + postId + "-" + votes[i][0];

                    html += '<input type="radio" id="' + radioId + '" name="flag-' + postId + '" value="' + votes[i][0] + '">'
                    html += '<label for="' + radioId + '">' + votes[i][1] + '</label><br>';
                }
            }

            html += '<div class="flag-comment">Why are you flagging this post?<textarea name="flag-reason" cols="33" rows="4"></textarea>'
            html += '<br><span class="text-counter"></span></div>';

            html += '</form></div>';
            html += '<a class="flag-cancel">Cancel</a><a class="flag-submit">Flag Post</a>';
            html += '</div>';

            var jDiv = $(html);
            var jForm = jDiv.find("#" + formId);
            var jTxt = jForm.find("textarea");

            // Show/hide submit link and textarea based on which radio is selected and validity..
            jForm.find("input").click(function() {
                var jCommentDiv = jForm.find("div.flag-comment");
                var isInform = vote.flagIsInform(jForm);

                jCommentDiv.toggle(isInform);
                if (isInform) jTxt.focus();
                vote.flagAllowSubmit(jDiv, jTxt, isInform);
            });

            // Check our textarea's validity for submission..
            jTxt.bind("blur focus keyup", function() {
                comments.updateTextCounter(this, 150, 10);
                vote.flagAllowSubmit(jDiv, jTxt, vote.flagIsInform(jForm));
            });

            // Only submit if we're valid..
            jDiv.find("a.flag-submit").click(function() {
                if (vote.flagIsInform(jForm) && !vote.flagTextValid(jTxt))
                    return;

                vote.flagSubmit(jClicked, postId, jForm, jTxt);
            });


            // Destroy this form when clicking cancel..
            jDiv.find("a.flag-cancel").click(function() { vote.flagClosePopup(jClicked); });

            // Show our hard work..
            jClicked.parent().append(jDiv);
            jDiv.fadeIn("fast");
        },

        flagIsInform: function(jForm) {
            var jInput = jForm.find("input:radio:checked");
            if (jInput.length == 0) return false;
            return jInput.val() == voteTypeIds.informModerator;
        },

        flagAllowSubmit: function(jDiv, jTxt, isInform) {
            var allow = isInform ? vote.flagTextValid(jTxt) : true;
            jDiv.find("a.flag-submit").toggle(allow);
        },

        flagTextValid: function(jTxt) {
            var count = $.trim(jTxt.val()).length;
            return (count >= 10 && count <= 150);
        },

        flagClosePopup: function(jClicked) {
            jClicked.parent().find(".vote-notification").fadeOut("fast", function() { $(this).remove(); });
        },

        flagSubmit: function(jClicked, postId, jForm, jTxt) {
            vote.flagClosePopup(jClicked);
            var vType = jForm.find("input:radio:checked").val();
            var postId = jClicked.attr('id').substring('flag-post-'.length);

            if (vType == voteTypeIds.informModerator) {
                $.ajax({
                    type: "POST",
                    url: '/messages/inform-moderator-about-post/' + postId,
                    dataType: "json",
                    data: { "fkey": fkey, "msg": jTxt.val() },
                    success: function(json) {
                        showAjaxError(jClicked.parent(), json.Message);
                    },
                    error: function(res, textStatus, errorThrown) {
                        showAjaxError(jClicked.parent(), (res.responseText && res.responseText.length < 100 ? res.responseText : "An error occurred during submission"));
                    }
                });
            }
            else {
                submit(jClicked, postId, vType, vote.flagSubmitCallback, { "comment": jTxt.val() });
            }
        },

        flagSubmitCallback: function(jClicked, postId, data) {
            if (data && data.Success) {
                if (data.Message) {
                }
            }
            else {
                var jDiv = jClicked.parent();
                if (data && data.Message)
                    showAjaxError(jDiv, data.Message);
                else
                    showAjaxError(jDiv, "A problem occurred during flagging");
            }
        },


        close: function(jClicked) {
            var isClosed = jClicked.text().indexOf("open") > -1;
            var postId = jClicked.attr("id").substring("close-question-".length);

            if (isClosed) {
                if (confirm("Nominate this question for reopening?"))
                    submit(jClicked, postId, voteTypeIds.reopen, vote.close_result);
            }
            else { // render a form with reasons for closing..
                if (!vote.close_reasons)
                    vote.close_fetchReasons(jClicked, postId);
                else
                    vote.close_renderForm(jClicked);
            }
        },
        
        close_reasons: null, // a json list of reasons with counts of existing close votes on the current question

        // [{"id":1,"name":"exact duplicate","description":"asdf","count":0},{"id":2,"name":"not programming related","description":"asdf","count":0},
        close_fetchReasons: function(jClicked, postId) {
            $(".error-notification").fadeOutAndRemove();
            appendLoader(jClicked);
            $.ajax({
                type: "GET",
                url: '/posts/close-reasons/' + postId,
                dataType: "json",
                success: function(json) {
                    removeLoader();
                    vote.close_reasons = json;
                    vote.close_renderForm(jClicked);
                },
                error: function(res, textStatus, errorThrown) {
                    removeLoader();
                    showAjaxError(jClicked.parent(), (res.responseText && res.responseText.length < 100 ? res.responseText : "An error occurred while fetching close reasons"));
                }
            });
        },

        close_renderForm: function(jClicked) {
            var postId = jClicked.attr("id").substring("close-question-".length);
            var popupId = 'close-popup-' + postId;
            
            $('#' + popupId).remove();
            
            var html = '<div id="' + popupId + '" class="vote-notification"><h2>Why should this question be closed?</h2><ul>';
            for (var i = 0; i < vote.close_reasons.length; i++) {
                var cr = vote.close_reasons[i];
                html += '<li><a id="close-reason-' + cr.id + '" class="close-reason"' +
                            (cr.description ? ' title="' + cr.description + '"' : '') + '>' + cr.name + '</a>' + 
                            (cr.count > 0 ? '<span title="this many votes already exist">(' + cr.count + ')</span>' : '') +
                        '</li>';
            }
            html += '</ul><a class="close-cancel">Cancel</a>';
            html += '</div>';
            var jDiv = $(html);

            jDiv.find("a.close-reason").click(function() { vote.close_reasonClick(jClicked, jDiv, $(this), postId); });
            jDiv.find("a.close-cancel").click(function() { jDiv.fadeOutAndRemove(); });

            jClicked.parent().append(jDiv);
            jDiv.fadeIn("fast");
        },

        close_reasonExactDuplicateId: 1,
        close_hasLinkedDuplicateQuestions: false,
        
        close_reasonClick: function(jClicked, jDiv, jReasonClicked, postId) {
            var reasonId = jReasonClicked.attr('id').substring('close-reason-'.length);

            // alter the form to allow picking of a question that this postId is a duplicate of
            if (reasonId == vote.close_reasonExactDuplicateId) {
                vote.close_duplicateForm(jClicked, jDiv, postId);
            }
            else {
                jDiv.fadeOutAndRemove();
                appendLoader(jClicked);
                submit(jClicked, postId, voteTypeIds.close, vote.close_result, { "close-reason-id": reasonId });
            }
        },
        
        close_duplicateForm: function(jClicked, jDiv, postId) {
            jDiv.find('h2, ul').remove(); // remove existing text in our div, making room for new content
            var textId = 'duplicate-question-' + postId; // textbox to enter question id or title searches
            
            var html = // new content for jDiv
            '<div style="padding-bottom:18px">' +
                '<h2>This question is a duplicate of which other question?</h2>' +
                '<input id="' + textId + '" type="text" size="78"><br>' +
                '<span>Type a question id or title to search for valid targets</span>' +
                '<div class="existing-linked-questions"></div>' +
            '</div>' +
            '<a class="close-submit">Vote to Close</a>';
            var jForm = $(html);
            
            jForm.find('#' + textId) 
                .autocomplete('/search/duplicate-questions/' + postId, { // set up ajax searches on text entered
                    highlightItem: true,
                    matchContains: true,
                    matchSubset: false,
                    scroll: true,
                    scrollHeight: 300,
                    formatItem: function(rowArray) {
                        // new searches should hide the submit link
                        jDiv.find('a.close-submit').hide();
                        
                        // server returns: 91061|J2EE App Server Hello World \n 489858|GUI &quot;Hello World&quot; examples in C
                        // autocomplete splits each row into an array: [Id,Title]
                        var id = rowArray[0];
                        var title = rowArray[1];
                        // server will pass a -1 for wrongful queries, with the title as reason
                        return (id == '-1') ? ('<b style="color:#990000">' + title + '</b>') : (id + ' - ' + title);
                    },
                    formatResult: function(rowArray) {
                        var id = rowArray[0];
                        // don't put anything in the text box when selecting a bad server result
                        return (id == '-1') ? ' ' : id;
                    }
                })
                .result(function(event, rowArray) { // this occurs when user clicks/tabs to a server result
                    // if we've selected valid data, show the submit link
                    jDiv.find('a.close-submit').toggle((rowArray && rowArray[0] != '-1'));
                });
            
            jDiv.prepend(jForm);
            vote.close_showExistingDuplicates(jDiv, postId); // load any other linked, master questions to previous, existing duplicate close votes
            jDiv.find('#' + textId).focus();            
            jDiv.find('a.close-submit').click(function() {
                jDiv.fadeOutAndRemove();
                
                // should have a valid question id in the box - server will also verify
                submit(jClicked, postId, voteTypeIds.close, vote.close_result, { 
                    "close-reason-id": vote.close_reasonExactDuplicateId,
                    "duplicate-question-id": $('#' + textId).val()
                });
            });
        },
        
        close_showExistingDuplicates: function(jDiv, postId) {
            var doFetch = false;
            for (var i = 0; i < vote.close_reasons.length; i++) {
                var cr = vote.close_reasons[i];
                if (cr.id == vote.close_reasonExactDuplicateId) {
                    if (cr.count > 0)
                        doFetch = true;
                    break;
                }
            }
            if (!doFetch) return;
            
            var jLinkedQuestions = jDiv.find('div.existing-linked-questions');
            appendLoader(jLinkedQuestions);
            
            jLinkedQuestions.append('<hr style="color:#fff; background-color:#fff;"><p>Other users chose these questions as the master question:</p>');
            
            // [{"url":"/questions/412314/asdf-or-other-module-system-...","title":"ASDF or other module system ..."}]
            $.getJSON('/posts/existing-close-duplicate-questions/' + postId, function(json) {
                removeLoader();
                for (var i = 0; i < json.length; i++) {
                    jLinkedQuestions.append('<li><a href="' + json[i].url + '" target="_blank">' + json[i].title + '</a></li>');
                }    
            });
        },

        close_result: function(jClicked, postId, data) {
            removeLoader();
            if (data && data.Success) {
                if (data.Message) {
                    var isClosed = jClicked.text().indexOf("open") > -1;
                    jClicked.text(jClicked.text().replace(/\w?\(\d\)/, "") + " " + data.Message);
                    showNotification(jClicked, "This question still needs " + data.NewScore + " vote(s) from other users to " +
                        (isClosed ? "reopen" : "close"));
                }
                else { // HACK: lack of message denotes a state change
                    location.reload(true);
                }
                vote.close_reasons = null; // reset so users can see effect of their vote
            }
            else {
                var jDiv = jClicked.parent();
                if (data && data.Message)
                    showAjaxError(jDiv, data.Message);
                else
                    showAjaxError(jDiv, "A problem occurred during closing/reopening");
            }

        },


        deletion: function(jClicked) {
            var postId = jClicked.attr("id").substring("delete-question-".length);
            var isDeleted = jClicked.text().indexOf("undelete") > -1;

            if (confirm("Vote to " + (isDeleted ? "un" : "") + "delete this post?")) {
                submit(jClicked, postId, (isDeleted ? voteTypeIds.undeletion : voteTypeIds.deletion), vote.deletionCallback);
            }
        },

        deletionCallback: function(jClicked, postId, data) {
            var wasDeleted = jClicked.text().indexOf("undelete") > -1;

            if (data && data.Success) {
                jClicked.text(data.Message);

                if (data.NewScore < 0) { // state change..
                    var isQuestion = $("#question:has(a[id='delete-question-" + postId + "'])").length > 0;
                    var selector = isQuestion ? "#question, div.answer" : "#answer-" + postId;
                    vote.setDeleteStyles($(selector), !wasDeleted);
                }
                else {
                    showNotification(jClicked, "This post still needs " + data.NewScore + " vote(s) from other users to " +
                        (wasDeleted ? "un" : "") + "delete");
                }
            }
            else {
                var msg = (data && data.Message) ? data.Message : "A problem occurred during " + (wasDeleted ? "un" : "") + "deletion";
                showAjaxError(jClicked.parent(), msg);
            }
        },

        setDeleteStyles: function(jDiv, isDeleted) {
            if (isDeleted) {
                $("div.question-status:has(span:contains('delete'))").show(); // probably not there..
                jDiv.addClass("deleted-answer").find("a[id^='delete-question-']").addClass("deleted-post").end()
                    .find("div[id^='comments-']").addClass("comments-container-deleted").end()
                    .find("a[id^='comments-link-']").addClass("comments-link-deleted");
            }
            else {
                // take the easy way out..
                document.location.reload(true);
            }
        },


        bountyStart: function(amount) {
            var questionId = $("#question div.vote input:first").val();

            $.post("/posts/" + questionId + "/bounty-start", { "fkey": fkey, "amount": amount }, function(data) {
                if (data.Success) {
                    location.reload(true);
                }
                else {
                    showNotification($("#bounty-errors"), data.Message);
                    $("#bounty-submit").attr("disabled", "");
                }
            }, "json");
        }

    };
} ();

function bindAllPostClicks() {
}

function unbindAllPostClicks() {
}

// site comments
var comments = function() {
    var flagImg = imagePath.replace("{0}", "comment-flag.png");
    var flagImgOver = imagePath.replace("{0}", "comment-flag-hover.png");
    var upImg = imagePath.replace("{0}", "comment-up.png");
    var upImgOver = imagePath.replace("{0}", "comment-up-hover.png");
    var delImg = imagePath.replace("{0}", "comment-del.png");
    var delImgOver = imagePath.replace("{0}", "comment-del-hover.png");

    var maxCommentLength = 600;

    var jDivInit = function(postId) {
        return $("#comments-" + postId);
    };

    var appendLoaderImg = function(postId) {
        appendLoader("#comments-" + postId + " div.comments");
    };

    var renderForm = function(postId, jDiv) {
        var jForm = $("#form-comments-" + postId);

        if (jForm.length > 0) {
            var form = '<table><tr><td><textarea name="comment" cols="68" rows="3" maxlength="' + maxCommentLength;
            form += '" onblur="comments.updateTextCounter(this)" ';
            form += 'onfocus="comments.updateTextCounter(this)" onkeyup="comments.updateTextCounter(this)"></textarea>';
            form += '<input type="submit" value="Add Comment" /></td></tr><tr><td><span class="text-counter"></span>';
            form += '<span class="form-error"></span></td></tr></table>';

            jForm.append(form);
            jForm.validate({
                rules: { comment: { required: true, minlength: 15} },
                errorElement: "span",
                errorClass: "form-error",
                errorPlacement: function(error, element) {
                    var span = element.parents("form").find("span.form-error");
                    span.replaceWith(error);
                },
                submitHandler: function(form) {
                    disableSubmitButton($(form));
                    postComment(postId, $(form));
                }
            });

            // line the form up with the comment text above it
            var actionWidth = $('#comments-' + postId + ' tr.comment:first td.comment-actions').width() || -1;
            actionWidth += 9; // a bit extra to account for padding
            jForm.children('table').css('margin-left', actionWidth + 'px');
        }
    };

    var fetchComments = function(postId, jDiv) {
        appendLoaderImg(postId);
        $.ajax({
            type: "GET",
            url: "/posts/" + postId + "/comments",
            dataType: "html",
            success: function(html) {
                showComments(postId, html);
            },
            error: function(res, textStatus, errorThrown) {
                removeLoader();
                showAjaxError("#comments-" + postId, (res.responseText && res.responseText.length < 100 ?
                    res.responseText : "An error has occured while fetching comments"));
            }
        });
    };

    // html will contain <table> of comments
    var showComments = function(postId, html) {
        var jDiv = jDivInit(postId).find("div.comments");

        // remove the existing comments
        if (jDiv.children().length > 0) {
            jDiv.children().remove();
        }
        jDiv.append(html);
        unbindImageEvents();
        bindImageEvents();
        removeLoader();
    };

    // this code exists also exists in ~/Views/Posts/Comments.ascx.cs -> AppendScoreCell
    var renderScore = function(score) {
        var result = "";
        if (score && score > 0) {
            var css = score < 5 ? '' : score <= 15 ? 'warm' : score <= 30 ? 'hot' : 'supernova';
            result += '<span title="number of \'great comment\' votes received" class="' + css + '">' + score + '</span>';
        }
        return result;
    };

    var bindImageEvents = function() {
        $("img.comment-up")
            .click(function() {
                submitVote($(this), 2, upImg, upImgOver, function(jImg, json) {
                    jImg.closest("tr").siblings("tr").remove(); // remove flag row
                    jImg.parent().siblings().children().remove(); // clean out existing score, if any
                    jImg.parent().siblings().append(renderScore(json.NewScore));
                });
            })
            .hover(
                function() { $(this).attr("src", upImgOver); },
                function() { $(this).attr("src", upImg); });

        $("img.comment-flag")
            .click(function() {
                if (confirm("Really flag this comment as offensive, spam, or hate speech?")) {
                    submitVote($(this), 4, flagImg, flagImgOver, function(jImg, json) {
                        if (json.NewScore == -1) // signal from server telling us comment has been deleted
                            jImg.parents("tr.comment").remove();
                        else {
                            jImg.parents("tr.comment").find("img.comment-up").remove();
                            jImg.remove();
                        }
                    });
                }
            })
            .hover(
                function() { $(this).attr("src", flagImgOver); },
                function() {
                    $(this).attr("src", flagImg);
                });

        $("img.comment-delete")
            .click(function() {
                if (confirm("Really delete this comment?")) {
                    submitVote($(this), 10, delImg, delImgOver, function(jImg, json) {
                        jImg.parents("tr.comment").remove();
                    });
                }
            })
            .hover(
                function() { $(this).attr("src", delImgOver); },
                function() {
                    $(this).attr("src", delImg);
                });

        // show/hide any clickable images and highlight the row
        $("tr.comment").hover(
            function() {
                $(this)
                    .find("img.comment-up, img.comment-flag, img.comment-delete").css("visibility", "visible")
                        .closest("tr.comment").addClass("comment-hover"); // this allows on rows with clickable images to be highlighted
            },
            function() {
                $(this)
                    .removeClass("comment-hover")
                    .find("img.comment-up, img.comment-flag, img.comment-delete").css("visibility", "hidden");
            }
        );
    };

    var unbindImageEvents = function() {
        $("tr.comment").unbind("mouseenter mouseleave");
        $("img.comment-up, img.comment-flag, img.comment-delete").unbind("click mouseenter mouseleave");
    };

    var submitVote = function(jImg, voteTypeId, imgSrc, imgSrcOver, successFunction) {
        var commentId = jImg.parents("tr.comment").attr("id").substr("comment-".length);
        var cssClass = jImg.attr("class");

        $("div.error-notification").hide();
        jImg.removeClass().unbind("click mouseenter mouseleave").attr("src", imgSrcOver);
        appendLoader(jImg.parent());

        var reattachHandler = function() {
            jImg
                .addClass(cssClass)
                .click(function() { submitVote(jImg, voteTypeId, imgSrc, imgSrcOver, successFunction); })
                .attr("src", imgSrc);
        };

        $.ajax({
            type: "POST",
            url: '/posts/comments/' + commentId + '/vote/' + voteTypeId,
            dataType: "json",
            data: { "fkey": fkey },
            success: function(json) {
                if (json.Success) {
                    successFunction(jImg, json);
                }
                else {
                    showAjaxError(jImg.parent(), json.Message);
                    reattachHandler();
                }
            },
            error: function(res, textStatus, errorThrown) {
                showAjaxError(jImg.parent(), (res.responseText && res.responseText.length < 100 ? res.responseText : "An error occurred during voting"));
                reattachHandler();
            }
        });
        removeLoader();
    };

    var postComment = function(postId, jForm) {
        var textarea = jForm.find("textarea");
        if (textarea.val() && textarea.val().length > maxCommentLength) {
            alert("Comments are limited to " + maxCommentLength + " characters");
            enableSubmitButton(formSelector);
            return;
        }

        appendLoaderImg(postId);
        var hideErrors = function() { $(".error-notification").fadeOut("fast", function() { $(this).remove(); }) };

        $.ajax({
            type: "POST",
            url: "/posts/" + postId + "/comments",
            dataType: "html",
            data: { comment: textarea.val(), "fkey": fkey },
            success: function(html) {
                hideErrors();
                showComments(postId, html);
                textarea.val("");
                comments.updateTextCounter(textarea);
                enableSubmitButton(jForm);
            },
            error: function(res, textStatus, errorThrown) {
                removeLoader();
                hideErrors();
                showAjaxError(jForm, (res.responseText && res.responseText.length < 100 ? res.responseText : "An error occurred during comment submission"));
                enableSubmitButton(jForm);
            }
        });
    };

    // public methods..
    return {

        init: function() {
            // Setup "show comments" clicks..
            $("a[id^='comments-link-']").unbind('click').click(function() {
                var postId = $(this).attr("id").substr("comments-link-".length);
                var jDiv = jDivInit(postId);

                renderForm(postId, jDiv); // render the form first, so we can set textarea focus if there are no comments on the server

                jDiv.removeClass("display-none");
                if ($(this).text().indexOf("more comment") > -1) {
                    fetchComments(postId, jDiv);
                }
                else {
                    // no comments, so help user out by focusing on the add comment textarea
                    jDiv.find('textarea').focus();
                }
                $(this).hide();
            });

            bindImageEvents();
        },

        updateTextCounter: function(textarea, maxLength, minLength) {
            minLength = minLength || 15;
            maxLength = maxLength || maxCommentLength;

            var length = textarea.value ? $.trim(textarea.value).length : 0;
            var jTxt = $(textarea);

            counter = jTxt.parents("form").find("span.text-counter");
            if(length == 0){
                counter.html("Enter at least {} characters.".replace("{}", minLength));
            } else if (length < minLength) {
                counter.html("{} more to go...".replace("{}", minLength - length));
            } else {
               counter.html((maxLength - length) + ' character' + (length < maxLength - 1 ? 's' : '') + ' left');
            }
        }
    };

} ();