// # Copyright (C) 2010-2020 RhodeCode GmbH
// #
// # This program is free software: you can redistribute it and/or modify
// # it under the terms of the GNU Affero General Public License, version 3
// # (only), as published by the Free Software Foundation.
// #
// # 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.
// #
// # You should have received a copy of the GNU Affero General Public License
// # along with this program.  If not, see <http://www.gnu.org/licenses/>.
// #
// # This program is dual-licensed. If you wish to learn more about the
// # RhodeCode Enterprise Edition, including its added features, Support services,
// # and proprietary license terms, please see https://rhodecode.com/licenses/

var firefoxAnchorFix = function() {
  // hack to make anchor links behave properly on firefox, in our inline
  // comments generation when comments are injected firefox is misbehaving
  // when jumping to anchor links
  if (location.href.indexOf('#') > -1) {
    location.href += '';
  }
};


var linkifyComments = function(comments) {
  var firstCommentId = null;
  if (comments) {
      firstCommentId = $(comments[0]).data('comment-id');
  }

  if (firstCommentId){
    $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
  }
};


var bindToggleButtons = function() {
  $('.comment-toggle').on('click', function() {
        $(this).parent().nextUntil('tr.line').toggle('inline-comments');
  });
};


var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
    failHandler = failHandler || function() {};
    postData = toQueryString(postData);
    var request = $.ajax({
        url: url,
        type: 'POST',
        data: postData,
        headers: {'X-PARTIAL-XHR': true}
    })
    .done(function (data) {
        successHandler(data);
    })
    .fail(function (data, textStatus, errorThrown) {
        failHandler(data, textStatus, errorThrown)
    });
    return request;
};


/* Comment form for main and inline comments */
(function(mod) {

    if (typeof exports == "object" && typeof module == "object") {
        // CommonJS
        module.exports = mod();
    }
    else {
        // Plain browser env
        (this || window).CommentForm = mod();
    }

})(function() {
    "use strict";

    function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {

        if (!(this instanceof CommentForm)) {
            return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
        }

        // bind the element instance to our Form
        $(formElement).get(0).CommentForm = this;

        this.withLineNo = function(selector) {
            var lineNo = this.lineNo;
            if (lineNo === undefined) {
                return selector
            } else {
                return selector + '_' + lineNo;
            }
        };

        this.commitId = commitId;
        this.pullRequestId = pullRequestId;
        this.lineNo = lineNo;
        this.initAutocompleteActions = initAutocompleteActions;

        this.previewButton = this.withLineNo('#preview-btn');
        this.previewContainer = this.withLineNo('#preview-container');

        this.previewBoxSelector = this.withLineNo('#preview-box');

        this.editButton = this.withLineNo('#edit-btn');
        this.editContainer = this.withLineNo('#edit-container');
        this.cancelButton = this.withLineNo('#cancel-btn');
        this.commentType = this.withLineNo('#comment_type');

        this.resolvesId = null;
        this.resolvesActionId = null;

        this.closesPr = '#close_pull_request';

        this.cmBox = this.withLineNo('#text');
        this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);

        this.statusChange = this.withLineNo('#change_status');

        this.submitForm = formElement;

        this.submitButton = $(this.submitForm).find('.submit-comment-action');
        this.submitButtonText = this.submitButton.val();

        this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
        this.submitDraftButtonText = this.submitDraftButton.val();

        this.previewUrl = pyroutes.url('repo_commit_comment_preview',
            {'repo_name': templateContext.repo_name,
             'commit_id': templateContext.commit_data.commit_id});

        if (edit){
            this.submitDraftButton.hide();
            this.submitButtonText = _gettext('Update Comment');
            $(this.commentType).prop('disabled', true);
            $(this.commentType).addClass('disabled');
            var editInfo =
                '';
            $(editInfo).insertBefore($(this.editButton).parent());
        }

        if (resolvesCommentId){
            this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
            this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
            $(this.commentType).prop('disabled', true);
            $(this.commentType).addClass('disabled');

            // disable select
            setTimeout(function() {
                $(self.statusChange).select2('readonly', true);
            }, 10);

            var resolvedInfo = (
                '<li class="resolve-action">' +
                '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
                '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
                '</li>'
            ).format(resolvesCommentId, _gettext('resolve comment'));
            $(resolvedInfo).insertAfter($(this.commentType).parent());
        }

        // based on commitId, or pullRequestId decide where do we submit
        // out data
        if (this.commitId){
            var pyurl = 'repo_commit_comment_create';
            if(edit){
                pyurl = 'repo_commit_comment_edit';
            }
            this.submitUrl = pyroutes.url(pyurl,
                {'repo_name': templateContext.repo_name,
                 'commit_id': this.commitId,
                 'comment_id': comment_id});
            this.selfUrl = pyroutes.url('repo_commit',
                {'repo_name': templateContext.repo_name,
                 'commit_id': this.commitId});

        } else if (this.pullRequestId) {
            var pyurl = 'pullrequest_comment_create';
            if(edit){
                pyurl = 'pullrequest_comment_edit';
            }
            this.submitUrl = pyroutes.url(pyurl,
                {'repo_name': templateContext.repo_name,
                 'pull_request_id': this.pullRequestId,
                 'comment_id': comment_id});
            this.selfUrl = pyroutes.url('pullrequest_show',
                {'repo_name': templateContext.repo_name,
                 'pull_request_id': this.pullRequestId});

        } else {
            throw new Error(
                'CommentForm requires pullRequestId, or commitId to be specified.')
        }

        // FUNCTIONS and helpers
        var self = this;

        this.isInline = function(){
            return this.lineNo && this.lineNo != 'general';
        };

        this.getCmInstance = function(){
            return this.cm
        };

        this.setPlaceholder = function(placeholder) {
            var cm = this.getCmInstance();
            if (cm){
                cm.setOption('placeholder', placeholder);
            }
        };

        this.getCommentStatus = function() {
          return $(this.submitForm).find(this.statusChange).val();
        };

        this.getCommentType = function() {
          return $(this.submitForm).find(this.commentType).val();
        };

        this.getDraftState = function () {
            var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
            var data = $(submitterElem).data('isDraft');
            return data
        }

        this.getResolvesId = function() {
            return $(this.submitForm).find(this.resolvesId).val() || null;
        };

        this.getClosePr = function() {
            return $(this.submitForm).find(this.closesPr).val() || null;
        };

        this.markCommentResolved = function(resolvedCommentId){
            Rhodecode.comments.markCommentResolved(resolvedCommentId)
        };

        this.isAllowedToSubmit = function() {
            var commentDisabled = $(this.submitButton).prop('disabled');
            var draftDisabled = $(this.submitDraftButton).prop('disabled');
            return !commentDisabled && !draftDisabled;
        };

        this.initStatusChangeSelector = function(){
            var formatChangeStatus = function(state, escapeMarkup) {
                var originalOption = state.element;
                var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
                return tmpl
            };
            var formatResult = function(result, container, query, escapeMarkup) {
                return formatChangeStatus(result, escapeMarkup);
            };

            var formatSelection = function(data, container, escapeMarkup) {
                return formatChangeStatus(data, escapeMarkup);
            };

            $(this.submitForm).find(this.statusChange).select2({
                placeholder: _gettext('Status Review'),
                formatResult: formatResult,
                formatSelection: formatSelection,
                containerCssClass: "drop-menu status_box_menu",
                dropdownCssClass: "drop-menu-dropdown",
                dropdownAutoWidth: true,
                minimumResultsForSearch: -1
            });

            $(this.submitForm).find(this.statusChange).on('change', function() {
                var status = self.getCommentStatus();

                if (status && !self.isInline()) {
                    $(self.submitButton).prop('disabled', false);
                    $(self.submitDraftButton).prop('disabled', false);
                }

                var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
                self.setPlaceholder(placeholderText)
            })
        };

        // reset the comment form into it's original state
        this.resetCommentFormState = function(content) {
            content = content || '';

            $(this.editContainer).show();
            $(this.editButton).parent().addClass('active');

            $(this.previewContainer).hide();
            $(this.previewButton).parent().removeClass('active');

            this.setActionButtonsDisabled(true);
            self.cm.setValue(content);
            self.cm.setOption("readOnly", false);

            if (this.resolvesId) {
                // destroy the resolve action
                $(this.resolvesId).parent().remove();
            }
            // reset closingPR flag
            $('.close-pr-input').remove();

            $(this.statusChange).select2('readonly', false);
        };

        this.globalSubmitSuccessCallback = function(comment){
            // default behaviour is to call GLOBAL hook, if it's registered.
            if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
                commentFormGlobalSubmitSuccessCallback(comment);
            }
        };

        this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
            return _submitAjaxPOST(url, postData, successHandler, failHandler);
        };

        // overwrite a submitHandler, we need to do it for inline comments
        this.setHandleFormSubmit = function(callback) {
            this.handleFormSubmit = callback;
        };

        // overwrite a submitSuccessHandler
        this.setGlobalSubmitSuccessCallback = function(callback) {
            this.globalSubmitSuccessCallback = callback;
        };

        // default handler for for submit for main comments
        this.handleFormSubmit = function() {
            var text = self.cm.getValue();
            var status = self.getCommentStatus();
            var commentType = self.getCommentType();
            var isDraft = self.getDraftState();
            var resolvesCommentId = self.getResolvesId();
            var closePullRequest = self.getClosePr();

            if (text === "" && !status) {
                return;
            }

            var excludeCancelBtn = false;
            var submitEvent = true;
            self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
            self.cm.setOption("readOnly", true);

            var postData = {
                'text': text,
                'changeset_status': status,
                'comment_type': commentType,
                'csrf_token': CSRF_TOKEN
            };

            if (resolvesCommentId) {
                postData['resolves_comment_id'] = resolvesCommentId;
            }

            if (closePullRequest) {
                postData['close_pull_request'] = true;
            }

            // submitSuccess for general comments
            var submitSuccessCallback = function(json_data) {
                // reload page if we change status for single commit.
                if (status && self.commitId) {
                    location.reload(true);
                } else {
                    // inject newly created comments, json_data is {<comment_id>: {}}
                    Rhodecode.comments.attachGeneralComment(json_data)

                    self.resetCommentFormState();
                    timeagoActivate();
                    tooltipActivate();

                    // mark visually which comment was resolved
                    if (resolvesCommentId) {
                        self.markCommentResolved(resolvesCommentId);
                    }
                }

                // run global callback on submit
                self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});

            };
            var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
                var prefix = "Error while submitting comment.\n"
                var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
                ajaxErrorSwal(message);
                self.resetCommentFormState(text);
            };
            self.submitAjaxPOST(
                self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
        };

        this.previewSuccessCallback = function(o) {
            $(self.previewBoxSelector).html(o);
            $(self.previewBoxSelector).removeClass('unloaded');

            // swap buttons, making preview active
            $(self.previewButton).parent().addClass('active');
            $(self.editButton).parent().removeClass('active');

            // unlock buttons
            self.setActionButtonsDisabled(false);
        };

        this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
            excludeCancelBtn = excludeCancelBtn || false;
            submitEvent = submitEvent || false;

            $(this.editButton).prop('disabled', state);
            $(this.previewButton).prop('disabled', state);

            if (!excludeCancelBtn) {
                $(this.cancelButton).prop('disabled', state);
            }

            var submitState = state;
            if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
                // if the value of commit review status is set, we allow
                // submit button, but only on Main form, isInline means inline
                submitState = false
            }

            $(this.submitButton).prop('disabled', submitState);
            $(this.submitDraftButton).prop('disabled', submitState);

            if (submitEvent) {
              var isDraft = self.getDraftState();

              if (isDraft) {
                $(this.submitDraftButton).val(_gettext('Saving Draft...'));
              }  else {
                $(this.submitButton).val(_gettext('Submitting...'));
              }

            } else {
              $(this.submitButton).val(this.submitButtonText);
              $(this.submitDraftButton).val(this.submitDraftButtonText);
            }

        };

        // lock preview/edit/submit buttons on load, but exclude cancel button
        var excludeCancelBtn = true;
        this.setActionButtonsDisabled(true, excludeCancelBtn);

        // anonymous users don't have access to initialized CM instance
        if (this.cm !== undefined){
            this.cm.on('change', function(cMirror) {
                if (cMirror.getValue() === "") {
                    self.setActionButtonsDisabled(true, excludeCancelBtn)
                } else {
                    self.setActionButtonsDisabled(false, excludeCancelBtn)
                }
            });
        }

        $(this.editButton).on('click', function(e) {
            e.preventDefault();

            $(self.previewButton).parent().removeClass('active');
            $(self.previewContainer).hide();

            $(self.editButton).parent().addClass('active');
            $(self.editContainer).show();

        });

        $(this.previewButton).on('click', function(e) {
            e.preventDefault();
            var text = self.cm.getValue();

            if (text === "") {
                return;
            }

            var postData = {
                'text': text,
                'renderer': templateContext.visual.default_renderer,
                'csrf_token': CSRF_TOKEN
            };

            // lock ALL buttons on preview
            self.setActionButtonsDisabled(true);

            $(self.previewBoxSelector).addClass('unloaded');
            $(self.previewBoxSelector).html(_gettext('Loading ...'));

            $(self.editContainer).hide();
            $(self.previewContainer).show();

            // by default we reset state of comment preserving the text
            var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
                var prefix = "Error while preview of comment.\n"
                var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
                ajaxErrorSwal(message);

                self.resetCommentFormState(text)
            };
            self.submitAjaxPOST(
                self.previewUrl, postData, self.previewSuccessCallback,
                previewFailCallback);

            $(self.previewButton).parent().addClass('active');
            $(self.editButton).parent().removeClass('active');
        });

        $(this.submitForm).submit(function(e) {
            e.preventDefault();
            var allowedToSubmit = self.isAllowedToSubmit();
            if (!allowedToSubmit){
               return false;
            }

            self.handleFormSubmit();
        });

    }

    return CommentForm;
});

/* selector for comment versions */
var initVersionSelector = function(selector, initialData) {

    var formatResult = function(result, container, query, escapeMarkup) {

        return renderTemplate('commentVersion', {
            show_disabled: true,
            version: result.comment_version,
            user_name: result.comment_author_username,
            gravatar_url: result.comment_author_gravatar,
            size: 16,
            timeago_component: result.comment_created_on,
        })
    };

    $(selector).select2({
        placeholder: "Edited",
        containerCssClass: "drop-menu-comment-history",
        dropdownCssClass: "drop-menu-dropdown",
        dropdownAutoWidth: true,
        minimumResultsForSearch: -1,
        data: initialData,
        formatResult: formatResult,
    });

    $(selector).on('select2-selecting', function (e) {
        // hide the mast as we later do preventDefault()
        $("#select2-drop-mask").click();
        e.preventDefault();
        e.choice.action();
    });

    $(selector).on("select2-open", function() {
        timeagoActivate();
    });
};

/* comments controller */
var CommentsController = function() {
  var mainComment = '#text';
  var self = this;

  this.showVersion = function (comment_id, comment_history_id) {

       var historyViewUrl = pyroutes.url(
           'repo_commit_comment_history_view',
           {
               'repo_name': templateContext.repo_name,
               'commit_id': null, // We don't need to check the commit data here...
               'comment_id': comment_id,
               'comment_history_id': comment_history_id,
           }
       );
       successRenderCommit = function (data) {
           SwalNoAnimation.fire({
               html: data,
               title: '',
           });
       };
       failRenderCommit = function () {
           SwalNoAnimation.fire({
               html: 'Error while loading comment history',
               title: '',
           });
       };
       _submitAjaxPOST(
           historyViewUrl, {'csrf_token': CSRF_TOKEN},
           successRenderCommit,
           failRenderCommit
       );
  };

  this.getLineNumber = function(node) {
      var $node = $(node);
      var lineNo = $node.closest('td').attr('data-line-no');
      if (lineNo === undefined && $node.data('commentInline')){
          lineNo = $node.data('commentLineNo')
      }

      return lineNo
  };

  this.scrollToComment = function(node, offset, outdated) {
    if (offset === undefined) {
      offset = 0;
    }
    var outdated = outdated || false;
    var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';

    if (!node) {
      node = $('.comment-selected');
      if (!node.length) {
        node = $('comment-current')
      }
    }

    $wrapper = $(node).closest('div.comment');

    // show hidden comment when referenced.
    if (!$wrapper.is(':visible')){
        $wrapper.show();
    }

    $comment = $(node).closest(klass);
    $comments = $(klass);

    $('.comment-selected').removeClass('comment-selected');

    var nextIdx = $(klass).index($comment) + offset;
    if (nextIdx >= $comments.length) {
      nextIdx = 0;
    }
    var $next = $(klass).eq(nextIdx);

    var $cb = $next.closest('.cb');
    $cb.removeClass('cb-collapsed');

    var $filediffCollapseState = $cb.closest('.filediff').prev();
    $filediffCollapseState.prop('checked', false);
    $next.addClass('comment-selected');
    scrollToElement($next);
    return false;
  };

  this.nextComment = function(node) {
    return self.scrollToComment(node, 1);
  };

  this.prevComment = function(node) {
    return self.scrollToComment(node, -1);
  };

  this.nextOutdatedComment = function(node) {
    return self.scrollToComment(node, 1, true);
  };

  this.prevOutdatedComment = function(node) {
    return self.scrollToComment(node, -1, true);
  };

  this.cancelComment = function (node) {
      var $node = $(node);
      var edit = $(this).attr('edit');
      var $inlineComments = $node.closest('div.inline-comments');

      if (edit) {
          var $general_comments = null;
          if (!$inlineComments.length) {
              $general_comments = $('#comments');
              var $comment = $general_comments.parent().find('div.comment:hidden');
              // show hidden general comment form
              $('#cb-comment-general-form-placeholder').show();
          } else {
              var $comment = $inlineComments.find('div.comment:hidden');
          }
          $comment.show();
      }
      var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
      $replyWrapper.removeClass('comment-form-active');

      var lastComment = $inlineComments.find('.comment-inline').last();
      if ($(lastComment).hasClass('comment-outdated')) {
        $replyWrapper.hide();
      }

      $node.closest('.comment-inline-form').remove();
      return false;
  };

  this._deleteComment = function(node) {
      var $node = $(node);
      var $td = $node.closest('td');
      var $comment = $node.closest('.comment');
      var comment_id = $($comment).data('commentId');
      var isDraft = $($comment).data('commentDraft');

      var pullRequestId = templateContext.pull_request_data.pull_request_id;
      var commitId = templateContext.commit_data.commit_id;

      if (pullRequestId) {
          var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
      } else if (commitId) {
          var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId})
      }

      var postData = {
        'csrf_token': CSRF_TOKEN
      };

      $comment.addClass('comment-deleting');
      $comment.hide('fast');

      var success = function(response) {
        $comment.remove();

        if (window.updateSticky !== undefined) {
            // potentially our comments change the active window size, so we
            // notify sticky elements
            updateSticky()
        }

        if (window.refreshAllComments !== undefined && !isDraft) {
          // if we have this handler, run it, and refresh all comments boxes
          refreshAllComments()
        }
        else if (window.refreshDraftComments !== undefined && isDraft) {
          // if we have this handler, run it, and refresh all comments boxes
          refreshDraftComments();
        }
        return false;
      };

      var failure = function(jqXHR, textStatus, errorThrown) {
        var prefix = "Error while deleting this comment.\n"
        var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
        ajaxErrorSwal(message);

        $comment.show('fast');
        $comment.removeClass('comment-deleting');
        return false;
      };
      ajaxPOST(url, postData, success, failure);

  }

  this.deleteComment = function(node) {
    var $comment = $(node).closest('.comment');
    var comment_id = $comment.attr('data-comment-id');

    SwalNoAnimation.fire({
      title: 'Delete this comment?',
      icon: 'warning',
      showCancelButton: true,
      confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),

    }).then(function(result) {
      if (result.value) {
        self._deleteComment(node);
      }
    })
  };

  this._finalizeDrafts = function(commentIds) {

    var pullRequestId = templateContext.pull_request_data.pull_request_id;
    var commitId = templateContext.commit_data.commit_id;

    if (pullRequestId) {
          var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
    } else if (commitId) {
          var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
    }

    // remove the drafts so we can lock them before submit.
    $.each(commentIds, function(idx, val){
        $('#comment-{0}'.format(val)).remove();
    })

    var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};

    var submitSuccessCallback = function(json_data) {
        self.attachInlineComment(json_data);

        if (window.refreshDraftComments !== undefined) {
          // if we have this handler, run it, and refresh all comments boxes
          refreshDraftComments()
        }

        if (window.refreshAllComments !== undefined) {
          // if we have this handler, run it, and refresh all comments boxes
          refreshAllComments()
        }

        return false;
    };

    ajaxPOST(url, postData, submitSuccessCallback)

  }

  this.finalizeDrafts = function(commentIds, callback) {

    SwalNoAnimation.fire({
      title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
      icon: 'warning',
      showCancelButton: true,
      confirmButtonText: _gettext('Yes'),

    }).then(function(result) {
      if (result.value) {
        if (callback !== undefined) {
            callback(result)
        }
        self._finalizeDrafts(commentIds);
      }
    })
  };

  this.toggleWideMode = function (node) {

      if ($('#content').hasClass('wrapper')) {
          $('#content').removeClass("wrapper");
          $('#content').addClass("wide-mode-wrapper");
          $(node).addClass('btn-success');
          return true
      } else {
          $('#content').removeClass("wide-mode-wrapper");
          $('#content').addClass("wrapper");
          $(node).removeClass('btn-success');
          return false
      }

  };

  /**
   * Turn off/on all comments in file diff
   */
  this.toggleDiffComments = function(node) {
    // Find closes filediff container
    var $filediff = $(node).closest('.filediff');
    if ($(node).hasClass('toggle-on')) {
        var show = false;
    } else if ($(node).hasClass('toggle-off')) {
        var show = true;
    }

    // Toggle each individual comment block, so we can un-toggle single ones
    $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
        self.toggleLineComments($(val), show)
    })

    // since we change the height of the diff container that has anchor points for upper
    // sticky header, we need to tell it to re-calculate those
    if (window.updateSticky !== undefined) {
        // potentially our comments change the active window size, so we
        // notify sticky elements
        updateSticky()
    }

    return false;
  }

  this.toggleLineComments = function(node, show) {

    var trElem = $(node).closest('tr')

    if (show === true) {
        // mark outdated comments as visible before the toggle;
        $(trElem).find('.comment-outdated').show();
        $(trElem).removeClass('hide-line-comments');
    } else if (show === false) {
        $(trElem).find('.comment-outdated').hide();
        $(trElem).addClass('hide-line-comments');
    } else {
        // mark outdated comments as visible before the toggle;
        $(trElem).find('.comment-outdated').show();
        $(trElem).toggleClass('hide-line-comments');
    }

    // since we change the height of the diff container that has anchor points for upper
    // sticky header, we need to tell it to re-calculate those
    if (window.updateSticky !== undefined) {
        // potentially our comments change the active window size, so we
        // notify sticky elements
        updateSticky()
    }

  };

  this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
      var pullRequestId = templateContext.pull_request_data.pull_request_id;
      var commitId = templateContext.commit_data.commit_id;

      var commentForm = new CommentForm(
          formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
      var cm = commentForm.getCmInstance();

      if (resolvesCommentId){
        placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
      }

      setTimeout(function() {
          // callbacks
          if (cm !== undefined) {
              commentForm.setPlaceholder(placeholderText);
              if (commentForm.isInline()) {
                cm.focus();
                cm.refresh();
              }
          }
      }, 10);

      // trigger scrolldown to the resolve comment, since it might be away
      // from the clicked
      if (resolvesCommentId){
        var actionNode = $(commentForm.resolvesActionId).offset();

        setTimeout(function() {
            if (actionNode) {
                $('body, html').animate({scrollTop: actionNode.top}, 10);
            }
        }, 100);
      }

        // add dropzone support
        var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
            var renderer = templateContext.visual.default_renderer;
            if (renderer == 'rst') {
                var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
                if (isRendered){
                    attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
                }
            } else if (renderer == 'markdown') {
                var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
                if (isRendered){
                    attachmentUrl = '!' + attachmentUrl;
                }
            } else {
                var attachmentUrl = '{}'.format(attachmentStoreUrl);
            }
            cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));

            return false;
        };

        //see: https://www.dropzonejs.com/#configuration
        var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
            {'repo_name': templateContext.repo_name,
                     'commit_id': templateContext.commit_data.commit_id})

        var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
        if (previewTmpl !== undefined){
            var selectLink = $(formElement).find('.pick-attachment').get(0);
            $(formElement).find('.comment-attachment-uploader').dropzone({
                url: storeUrl,
                headers: {"X-CSRF-Token": CSRF_TOKEN},
                paramName: function () {
                    return "attachment"
                }, // The name that will be used to transfer the file
                clickable: selectLink,
                parallelUploads: 1,
                maxFiles: 10,
                maxFilesize: templateContext.attachment_store.max_file_size_mb,
                uploadMultiple: false,
                autoProcessQueue: true, // if false queue will not be processed automatically.
                createImageThumbnails: false,
                previewTemplate: previewTmpl.innerHTML,

                accept: function (file, done) {
                    done();
                },
                init: function () {

                    this.on("sending", function (file, xhr, formData) {
                        $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
                        $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
                    });

                    this.on("success", function (file, response) {
                        $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
                        $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();

                        var isRendered = false;
                        var ext = file.name.split('.').pop();
                        var imageExts = templateContext.attachment_store.image_ext;
                        if (imageExts.indexOf(ext) !== -1){
                            isRendered = true;
                        }

                        insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
                    });

                    this.on("error", function (file, errorMessage, xhr) {
                        $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();

                        var error = null;

                        if (xhr !== undefined){
                            var httpStatus = xhr.status + " " + xhr.statusText;
                            if (xhr !== undefined && xhr.status >= 500) {
                                error = httpStatus;
                            }
                        }

                        if (error === null) {
                            error = errorMessage.error || errorMessage || httpStatus;
                        }
                        $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));

                    });
                }
            });
        }
      return commentForm;
  };

  this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {

      var tmpl = $('#cb-comment-general-form-template').html();
      tmpl = tmpl.format(null, 'general');
      var $form = $(tmpl);

      var $formPlaceholder = $('#cb-comment-general-form-placeholder');
      var curForm = $formPlaceholder.find('form');
      if (curForm){
          curForm.remove();
      }
      $formPlaceholder.append($form);

      var _form = $($form[0]);
      var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
      var edit = false;
      var comment_id = null;
      var commentForm = this.createCommentForm(
          _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
      commentForm.initStatusChangeSelector();

      return commentForm;
  };

  this.editComment = function(node, line_no, f_path) {
      self.edit = true;
      var $node = $(node);
      var $td = $node.closest('td');

      var $comment = $(node).closest('.comment');
      var comment_id = $($comment).data('commentId');
      var isDraft = $($comment).data('commentDraft');
      var $editForm = null

      var $comments = $node.closest('div.inline-comments');
      var $general_comments = null;

      if($comments.length){
          // inline comments setup
          $editForm = $comments.find('.comment-inline-form');
          line_no = self.getLineNumber(node)
      }
      else{
          // general comments setup
          $comments = $('#comments');
          $editForm = $comments.find('.comment-inline-form');
          line_no = $comment[0].id
          $('#cb-comment-general-form-placeholder').hide();
      }

      if ($editForm.length === 0) {

          // unhide all comments if they are hidden for a proper REPLY mode
          var $filediff = $node.closest('.filediff');
          $filediff.removeClass('hide-comments');

          $editForm = self.createNewFormWrapper(f_path, line_no);
          if(f_path && line_no) {
            $editForm.addClass('comment-inline-form-edit')
          }

          $comment.after($editForm)

          var _form = $($editForm[0]).find('form');
          var autocompleteActions = ['as_note',];
          var commentForm = this.createCommentForm(
              _form, line_no, '', autocompleteActions, resolvesCommentId,
              this.edit, comment_id);
          var old_comment_text_binary = $comment.attr('data-comment-text');
          var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
          commentForm.cm.setValue(old_comment_text);
          $comment.hide();
          tooltipActivate();

           // set a CUSTOM submit handler for inline comment edit action.
           commentForm.setHandleFormSubmit(function(o) {
              var text = commentForm.cm.getValue();
              var commentType = commentForm.getCommentType();

              if (text === "") {
                return;
              }

              if (old_comment_text == text) {
                  SwalNoAnimation.fire({
                      title: 'Unable to edit comment',
                      html: _gettext('Comment body was not changed.'),
                  });
                  return;
              }
              var excludeCancelBtn = false;
              var submitEvent = true;
              commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
              commentForm.cm.setOption("readOnly", true);

              // Read last version known
              var versionSelector = $('#comment_versions_{0}'.format(comment_id));
              var version = versionSelector.data('lastVersion');

              if (!version) {
                version = 0;
              }

              var postData = {
                  'text': text,
                  'f_path': f_path,
                  'line': line_no,
                  'comment_type': commentType,
                  'draft': isDraft,
                  'version': version,
                  'csrf_token': CSRF_TOKEN
              };

              var submitSuccessCallback = function(json_data) {
                $editForm.remove();
                $comment.show();
                var postData = {
                    'text': text,
                    'renderer': $comment.attr('data-comment-renderer'),
                    'csrf_token': CSRF_TOKEN
                };

                /* Inject new edited version selector */
                var updateCommentVersionDropDown = function () {
                    var versionSelectId = '#comment_versions_'+comment_id;
                    var preLoadVersionData = [
                        {
                            id: json_data['comment_version'],
                            text: "v{0}".format(json_data['comment_version']),
                            action: function () {
                                Rhodecode.comments.showVersion(
                                    json_data['comment_id'],
                                    json_data['comment_history_id']
                                )
                            },
                            comment_version: json_data['comment_version'],
                            comment_author_username: json_data['comment_author_username'],
                            comment_author_gravatar: json_data['comment_author_gravatar'],
                            comment_created_on: json_data['comment_created_on'],
                        },
                    ]


                    if ($(versionSelectId).data('select2')) {
                        var oldData = $(versionSelectId).data('select2').opts.data.results;
                        $(versionSelectId).select2("destroy");
                        preLoadVersionData = oldData.concat(preLoadVersionData)
                    }

                    initVersionSelector(versionSelectId, {results: preLoadVersionData});

                    $comment.attr('data-comment-text', utf8ToB64(text));

                    var versionSelector = $('#comment_versions_'+comment_id);

                    // set lastVersion so we know our last edit version
                    versionSelector.data('lastVersion', json_data['comment_version'])
                    versionSelector.parent().show();
                }
                updateCommentVersionDropDown();

                // by default we reset state of comment preserving the text
                var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
                    var prefix = "Error while editing this comment.\n"
                    var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
                    ajaxErrorSwal(message);
                };

                var successRenderCommit = function(o){
                    $comment.show();
                    $comment[0].lastElementChild.innerHTML = o;
                };

                var previewUrl = pyroutes.url(
                    'repo_commit_comment_preview',
             {'repo_name': templateContext.repo_name,
                    'commit_id': templateContext.commit_data.commit_id});

                _submitAjaxPOST(
                    previewUrl, postData, successRenderCommit, failRenderCommit
                );

                try {
                  var html = json_data.rendered_text;
                  var lineno = json_data.line_no;
                  var target_id = json_data.target_id;

                  $comments.find('.cb-comment-add-button').before(html);

                 //  run global callback on submit
                  commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});

                } catch (e) {
                  console.error(e);
                }

                // re trigger the linkification of next/prev navigation
                linkifyComments($('.inline-comment-injected'));
                timeagoActivate();
                tooltipActivate();

                if (window.updateSticky !== undefined) {
                    // potentially our comments change the active window size, so we
                    // notify sticky elements
                    updateSticky()
                }

                if (window.refreshAllComments !== undefined && !isDraft) {
                      // if we have this handler, run it, and refresh all comments boxes
                      refreshAllComments()
                }
               else if (window.refreshDraftComments !== undefined && isDraft) {
                  // if we have this handler, run it, and refresh all comments boxes
                  refreshDraftComments();
               }

                commentForm.setActionButtonsDisabled(false);

              };

              var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
                  var prefix = "Error while editing comment.\n"
                  var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
                  if (jqXHR.status == 409){
                    message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
                    ajaxErrorSwal(message, 'Comment version mismatch.');
                  } else {
                    ajaxErrorSwal(message);
                  }

                  commentForm.resetCommentFormState(text)
              };
              commentForm.submitAjaxPOST(
                  commentForm.submitUrl, postData,
                  submitSuccessCallback,
                  submitFailCallback);
            });
      }

      $editForm.addClass('comment-inline-form-open');
  };

  this.attachComment = function(json_data) {
    var self = this;
    $.each(json_data, function(idx, val) {
        var json_data_elem = [val]
        var isInline = val.comment_f_path && val.comment_lineno

        if (isInline) {
            self.attachInlineComment(json_data_elem)
        }  else {
            self.attachGeneralComment(json_data_elem)
        }
    })

  }

  this.attachGeneralComment = function(json_data) {
    $.each(json_data, function(idx, val) {
        $('#injected_page_comments').append(val.rendered_text);
    })
  }

  this.attachInlineComment = function(json_data) {

    $.each(json_data, function (idx, val) {
        var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
        var html = val.rendered_text;
        var $inlineComments = $('#' + val.target_id)
                .find(line_qry)
                .find('.inline-comments');

        var lastComment = $inlineComments.find('.comment-inline').last();

        if (lastComment.length === 0) {
            // first comment, we append simply
            $inlineComments.find('.reply-thread-container-wrapper').before(html);
        } else {
            $(lastComment).after(html)
        }

    })

  };

  this.createNewFormWrapper = function(f_path, line_no) {
      // create a new reply HTML form from template
      var tmpl = $('#cb-comment-inline-form-template').html();
      tmpl = tmpl.format(escapeHtml(f_path), line_no);
      return $(tmpl);
  }

  this.markCommentResolved = function(commentId) {
    $('#comment-label-{0}'.format(commentId)).find('.resolved').show();
    $('#comment-label-{0}'.format(commentId)).find('.resolve').hide();
  };

  this.createComment = function(node, f_path, line_no, resolutionComment) {
      self.edit = false;
      var $node = $(node);
      var $td = $node.closest('td');
      var resolvesCommentId = resolutionComment || null;

      var $replyForm = $td.find('.comment-inline-form');

      // if form isn't existing, we're generating a new one and injecting it.
      if ($replyForm.length === 0) {

          // unhide/expand all comments if they are hidden for a proper REPLY mode
          self.toggleLineComments($node, true);

          $replyForm = self.createNewFormWrapper(f_path, line_no);

          var $comments = $td.find('.inline-comments');

          // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
          if ($comments.length===0) {
            var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(escapeHtml(f_path), line_no)
            var $reply_container = $('#cb-comments-inline-container-template')
            $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
            $td.append($($reply_container).html());
          }

          // default comment button exists, so we prepend the form for leaving initial comment
          $td.find('.cb-comment-add-button').before($replyForm);
          // set marker, that we have a open form
          var $replyWrapper = $td.find('.reply-thread-container-wrapper')
          $replyWrapper.addClass('comment-form-active');

          var lastComment = $comments.find('.comment-inline').last();
          if ($(lastComment).hasClass('comment-outdated')) {
            $replyWrapper.show();
          }

          var _form = $($replyForm[0]).find('form');
          var autocompleteActions = ['as_note', 'as_todo'];
          var comment_id=null;
          var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
          var commentForm = self.createCommentForm(
              _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
              self.edit, comment_id);

          // set a CUSTOM submit handler for inline comments.
          commentForm.setHandleFormSubmit(function(o) {
            var text = commentForm.cm.getValue();
            var commentType = commentForm.getCommentType();
            var resolvesCommentId = commentForm.getResolvesId();
            var isDraft = commentForm.getDraftState();

            if (text === "") {
              return;
            }

            if (line_no === undefined) {
              alert('Error: unable to fetch line number for this inline comment !');
              return;
            }

            if (f_path === undefined) {
              alert('Error: unable to fetch file path for this inline comment !');
              return;
            }

            var excludeCancelBtn = false;
            var submitEvent = true;
            commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
            commentForm.cm.setOption("readOnly", true);
            var postData = {
                'text': text,
                'f_path': f_path,
                'line': line_no,
                'comment_type': commentType,
                'draft': isDraft,
                'csrf_token': CSRF_TOKEN
            };
            if (resolvesCommentId){
                postData['resolves_comment_id'] = resolvesCommentId;
            }

            // submitSuccess for inline commits
            var submitSuccessCallback = function(json_data) {

                $replyForm.remove();
                $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');

                try {

                    // inject newly created comments, json_data is {<comment_id>: {}}
                    self.attachInlineComment(json_data)

                    //mark visually which comment was resolved
                    if (resolvesCommentId) {
                        self.markCommentResolved(resolvesCommentId);
                    }

                    // run global callback on submit
                    commentForm.globalSubmitSuccessCallback({
                        draft: isDraft,
                        comment_id: comment_id
                    });

                } catch (e) {
                    console.error(e);
                }

              if (window.updateSticky !== undefined) {
                  // potentially our comments change the active window size, so we
                  // notify sticky elements
                  updateSticky()
              }

              if (window.refreshAllComments !== undefined && !isDraft) {
                  // if we have this handler, run it, and refresh all comments boxes
                  refreshAllComments()
              }
              else if (window.refreshDraftComments !== undefined && isDraft) {
                  // if we have this handler, run it, and refresh all comments boxes
                  refreshDraftComments();
              }

              commentForm.setActionButtonsDisabled(false);

              // re trigger the linkification of next/prev navigation
              linkifyComments($('.inline-comment-injected'));
              timeagoActivate();
              tooltipActivate();
            };

            var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
                var prefix = "Error while submitting comment.\n"
                var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
                ajaxErrorSwal(message);
                commentForm.resetCommentFormState(text)
            };

            commentForm.submitAjaxPOST(
                commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
          });
      }

      // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
      $replyForm.addClass('comment-inline-form-open');
      tooltipActivate();
  };

  this.createResolutionComment = function(commentId){
    // hide the trigger text
    $('#resolve-comment-{0}'.format(commentId)).hide();

    var comment = $('#comment-'+commentId);
    var commentData = comment.data();

    if (commentData.commentInline) {
        var f_path = commentData.commentFPath;
        var line_no = commentData.commentLineNo;
        this.createComment(comment, f_path, line_no, commentId)
    } else {
        this.createGeneralComment('general', "$placeholder", commentId)
    }

    return false;
  };

  this.submitResolution = function(commentId){
    var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
    var commentForm = form.get(0).CommentForm;

    var cm = commentForm.getCmInstance();
    var renderer = templateContext.visual.default_renderer;
    if (renderer == 'rst'){
        var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
    } else if (renderer == 'markdown') {
        var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
    } else {
        var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
    }

    cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
    form.submit();
    return false;
  };

  this.resolveTodo = function (elem, todoId) {
      var commentId = todoId;

      SwalNoAnimation.fire({
          title: 'Resolve TODO {0}'.format(todoId),
          showCancelButton: true,
          confirmButtonText: _gettext('Yes'),
          showLoaderOnConfirm: true,

          allowOutsideClick: function () {
              !Swal.isLoading()
          },
          preConfirm: function () {
              var comment = $('#comment-' + commentId);
              var commentData = comment.data();

              var f_path = null
              var line_no = null
              if (commentData.commentInline) {
                  f_path = commentData.commentFPath;
                  line_no = commentData.commentLineNo;
              }

              var renderer = templateContext.visual.default_renderer;
              var commentBoxUrl = '{1}#comment-{0}'.format(commentId);

              // Pull request case
              if (templateContext.pull_request_data.pull_request_id !== null) {
                  var commentUrl = pyroutes.url('pullrequest_comment_create',
                          {
                              'repo_name': templateContext.repo_name,
                              'pull_request_id': templateContext.pull_request_data.pull_request_id,
                              'comment_id': commentId
                          });
              } else {
                  var commentUrl = pyroutes.url('repo_commit_comment_create',
                          {
                              'repo_name': templateContext.repo_name,
                              'commit_id': templateContext.commit_data.commit_id,
                              'comment_id': commentId
                          });
              }

              if (renderer === 'rst') {
                  commentBoxUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentUrl);
              } else if (renderer === 'markdown') {
                  commentBoxUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentUrl);
              }
              var resolveText = _gettext('TODO from comment {0} was fixed.').format(commentBoxUrl);

              var postData = {
                  text: resolveText,
                  comment_type: 'note',
                  draft: false,
                  csrf_token: CSRF_TOKEN,
                  resolves_comment_id: commentId
              }
              if (commentData.commentInline) {
                  postData['f_path'] = f_path;
                  postData['line'] = line_no;
              }

              return new Promise(function (resolve, reject) {
                  $.ajax({
                      type: 'POST',
                      data: postData,
                      url: commentUrl,
                      headers: {'X-PARTIAL-XHR': true}
                  })
                  .done(function (data) {
                      resolve(data);
                  })
                  .fail(function (jqXHR, textStatus, errorThrown) {
                      var prefix = "Error while resolving TODO.\n"
                      var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
                      ajaxErrorSwal(message);
                  });
              })
          }

      })
      .then(function (result) {
          var success = function (json_data) {
              resolvesCommentId = commentId;
              var commentResolved = json_data[Object.keys(json_data)[0]]

              try {

                  if (commentResolved.f_path) {
                      // inject newly created comments, json_data is {<comment_id>: {}}
                      self.attachInlineComment(json_data)
                  } else {
                      self.attachGeneralComment(json_data)
                  }

                  //mark visually which comment was resolved
                  if (resolvesCommentId) {
                      self.markCommentResolved(resolvesCommentId);
                  }

                  // run global callback on submit
                  if (window.commentFormGlobalSubmitSuccessCallback !== undefined) {
                      commentFormGlobalSubmitSuccessCallback({
                          draft: false,
                          comment_id: commentId
                      });
                  }

              } catch (e) {
                  console.error(e);
              }

              if (window.updateSticky !== undefined) {
                  // potentially our comments change the active window size, so we
                  // notify sticky elements
                  updateSticky()
              }

              if (window.refreshAllComments !== undefined) {
                  // if we have this handler, run it, and refresh all comments boxes
                  refreshAllComments()
              }
              // re trigger the linkification of next/prev navigation
              linkifyComments($('.inline-comment-injected'));
              timeagoActivate();
              tooltipActivate();
          };

          if (result.value) {
            $(elem).remove();
            success(result.value)
          }
      })
  };

};

window.commentHelp = function(renderer) {
    var funcData = {'renderer': renderer}
    return renderTemplate('commentHelpHovercard', funcData)
}
