// Copyright 2008 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Generic helpers
/**
 * Create a new XMLHttpRequest in a cross-browser-compatible way.
 * @return XMLHttpRequest object
 */
function M_getXMLHttpRequest() {
  try {
    return new XMLHttpRequest();
  } catch (e) { }
  try {
    return new ActiveXObject("Msxml2.XMLHTTP");
  } catch (e) { }
  try {
    return new ActiveXObject("Microsoft.XMLHTTP");
  } catch (e) { }
  return null;
}
/**
 * Finds the element's parent in the DOM tree.
 * @param {Element} element The element whose parent we want to find
 * @return The parent element of the given element
 */
function M_getParent(element) {
  if (element.parentNode) {
    return element.parentNode;
  } else if (element.parentElement) {
    // IE compatibility. Why follow standards when you can make up your own?
    return element.parentElement;
  }
  return null;
}
/**
 * Finds the event's target in a way that works on all browsers.
 * @param {Event} e The event object whose target we want to find
 * @return The element receiving the event
 */
function M_getEventTarget(e) {
  var src = e.srcElement ? e.srcElement : e.target;
  return src;
}
/**
 * Function to determine if we are in a KHTML-based browser(Konq/Safari).
 * @return Boolean of whether we are in a KHTML browser
 */
function M_isKHTML() {
  var agt = navigator.userAgent.toLowerCase();
  return (agt.indexOf("safari") != -1) || (agt.indexOf("khtml") != -1);
}
/**
 * Function to determine if we are running in an IE browser.
 * @return Boolean of whether we are running in IE
 */
function M_isIE() {
  return (navigator.userAgent.toLowerCase().indexOf("msie") != -1) &&
         !window.opera;
}
/**
 * Function to determine if we are in a WebKit-based browser (Chrome/Safari).
 * @return Boolean of whether we are in a WebKit browser
 */
function M_isWebKit() {
  return navigator.userAgent.toLowerCase().indexOf("webkit") != -1;
}
/**
 * Stop the event bubbling in a browser-independent way. Sometimes required
 * when it is not easy to return true when an event is handled.
 * @param {Window} win The window in which this event is happening
 * @param {Event} e The event that we want to cancel
 */
function M_stopBubble(win, e) {
  if (!e) {
    e = win.event;
  }
  e.cancelBubble = true;
  if (e.stopPropagation) {
    e.stopPropagation();
  }
}
/**
 * Return distance in pixels from the top of the document to the given element.
 * @param {Element} element The element whose offset we want to find
 * @return Integer value of the height of the element from the top
 */
function M_getPageOffsetTop(element) {
  var y = element.offsetTop;
  if (element.offsetParent != null) {
    y += M_getPageOffsetTop(element.offsetParent);
  }
  return y;
}
function M_editPatchsetTitle(issue, patchset, xsrf_token,
                             original_patchset_title, patch_count) {
  var new_patchset_title = prompt(
      'Please enter the new title of Patch Set ' + patch_count,
      original_patchset_title);
  if (new_patchset_title == null) { 
    return false;
  } else if (new_patchset_title == original_patchset_title) {
    // Do not make an HTTP req if the new specified title is exactly the same.
    return false;
  }
  //Build POST data for request.
  var data = [];
  data.push('xsrf_token=' + xsrf_token);
  data.push('patchset_title=' + new_patchset_title);
  var httpreq = M_getXMLHttpRequest();
  if (!httpreq) {
    return true;
  }
  httpreq.onreadystatechange = function() {
    if (httpreq.readyState == 4) {
      if (httpreq.status == 200) {
        window.location.reload();
      }
    }
  }
  httpreq.open(
      'POST',
       base_url + issue + "/patchset/" + patchset + '/edit_patchset_title',
       true);
  httpreq.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  httpreq.send(data.join("&"));
  return true;
}
/**
 * Return distance in pixels of the given element from the left of the document.
 * @param {Element} element The element whose offset we want to find
 * @return Integer value of the horizontal position of the element
 */
function M_getPageOffsetLeft(element) {
  var x = element.offsetLeft;
  if (element.offsetParent != null) {
    x += M_getPageOffsetLeft(element.offsetParent);
  }
  return x;
}
/**
 * Find the height of the window viewport.
 * @param {Window} win The window whose viewport we would like to measure
 * @return Integer value of the height of the given window
 */
function M_getWindowHeight(win) {
  return M_getWindowPropertyByBrowser_(win, M_getWindowHeightGetters_);
}
/**
 * Find the vertical scroll position of the given window.
 * @param {Window} win The window whose scroll position we want to find
 * @return Integer value of the scroll position of the given window
 */
function M_getScrollTop(win) {
  return M_getWindowPropertyByBrowser_(win, M_getScrollTopGetters_);
}
/**
 * Scroll the target element into view at 1/3rd of the window height only if
 * the scrolling direction matches the direction that was asked for.
 * @param {Window} win The window in which the element resides
 * @param {Element} element The element that we want to bring into view
 * @param {Integer} direction Positive for scroll down, negative for scroll up
 */
function M_scrollIntoView(win, element, direction) {
  var elTop = M_getPageOffsetTop(element);
  var winHeight = M_getWindowHeight(win);
  var targetScroll = elTop - winHeight / 3;
  var scrollTop = M_getScrollTop(win);
  if ((direction > 0 && scrollTop < targetScroll) ||
      (direction < 0 && scrollTop > targetScroll)) {
    win.scrollTo(M_getPageOffsetLeft(element), targetScroll);
  }
}
/**
 * Returns whether the element is visible.
 * @param {Window} win The window that the element resides in
 * @param {Element} element The element whose visibility we want to determine
 * @return Boolean of whether the element is visible in the window or not
 */
function M_isElementVisible(win, element) {
  var elTop = M_getPageOffsetTop(element);
  var winHeight = M_getWindowHeight(win);
  var winTop = M_getScrollTop(win);
  if (elTop < winTop || elTop > winTop + winHeight) {
    return false;
  }
  return true;
}
// Cross-browser compatibility quirks and methodology borrowed from
// common.js
var M_getWindowHeightGetters_ = {
  ieQuirks_: function(win) {
    return win.document.body.clientHeight;
  },
  ieStandards_: function(win) {
    return win.document.documentElement.clientHeight;
  },
  dom_: function(win) {
    return win.innerHeight;
  }
};
var M_getScrollTopGetters_ = {
  ieQuirks_: function(win) {
    return win.document.body.scrollTop;
  },
  ieStandards_: function(win) {
    return win.document.documentElement.scrollTop;
  },
  dom_: function(win) {
    return win.pageYOffset;
  }
};
/**
 * Slightly modified from common.js: Konqueror has the CSS1Compat property
 * but requires the standard DOM functionlity, not the IE one.
 */
function M_getWindowPropertyByBrowser_(win, getters) {
  try {
    if (!M_isKHTML() && "compatMode" in win.document &&
        win.document.compatMode == "CSS1Compat") {
      return getters.ieStandards_(win);
    } else if (M_isIE()) {
      return getters.ieQuirks_(win);
    }
  } catch (e) {
    // Ignore for now and fall back to DOM method
  }
  return getters.dom_(win);
}
// Global search box magic (global.html)
/**
 * Handle the onblur action of the search box, replacing it with greyed out
 * instruction text when it is empty.
 * @param {Element} element The search box element
 */
function M_onSearchBlur(element) {
  var defaultMsg = "Enter a changelist#, user, or group";
  if (element.value.length == 0 || element.value == defaultMsg) {
    element.style.color = "gray";
    element.value = defaultMsg;
  } else {
    element.style.color = "";
  }
}
/**
 * Handle the onfocus action of the search box, emptying it out if no new text
 * was entered.
 * @param {Element} element The search box element
 */
function M_onSearchFocus(element) {
  if (element.style.color == "gray") {
    element.style.color = "";
    element.value = "";
  }
}
// Inline diffs (changelist.html)
/**
 * Creates an iframe to load the diff in the background and when that's done,
 * calls a function to transfer the contents of the iframe into the current DOM.
 * @param {Integer} suffix The number associated with that diff
 * @param {String} url The URL that the diff should be fetched from
 * @return false (for event bubbling purposes)
 */
function M_showInlineDiff(suffix, url) {
  var hide = document.getElementById("hide-" + suffix);
  var show = document.getElementById("show-" + suffix);
  var frameDiv = document.getElementById("frameDiv-" + suffix);
  var dumpDiv = document.getElementById("dumpDiv-" + suffix);
  var diffTR = document.getElementById("diffTR-" + suffix);
  var hideAll = document.getElementById("hide-alldiffs");
  var showAll = document.getElementById("show-alldiffs");
  /* Twiddle the "show/hide all diffs" link */
  if (hide.style.display != "") {
    M_CL_hiddenInlineDiffCount -= 1;
    if (M_CL_hiddenInlineDiffCount == M_CL_maxHiddenInlineDiffCount) {
      showAll.style.display = "inline";
      hideAll.style.display = "none";
    } else {
      showAll.style.display = "none";
      hideAll.style.display = "inline";
    }
  }
  hide.style.display = "";
  show.style.display = "none";
  dumpDiv.style.display = "block"; // XXX why not ""?
  diffTR.style.display = "";
  if (!frameDiv.innerHTML) {
    if (M_isKHTML()) {
      frameDiv.style.display = "block"; // XXX why not ""?
    }
    frameDiv.innerHTML = "";
  }
  return false;
}
/**
 * Hides the diff that was retrieved with M_showInlineDiff.
 * @param {Integer} suffix The number associated with the diff we want to hide
 */
function M_hideInlineDiff(suffix) {
  var hide = document.getElementById("hide-" + suffix);
  var show = document.getElementById("show-" + suffix);
  var dumpDiv = document.getElementById("dumpDiv-" + suffix);
  var diffTR = document.getElementById("diffTR-" + suffix);
  var hideAll = document.getElementById("hide-alldiffs");
  var showAll = document.getElementById("show-alldiffs");
  /* Twiddle the "show/hide all diffs" link */
  if (hide.style.display != "none") {
    M_CL_hiddenInlineDiffCount += 1;
    if (M_CL_hiddenInlineDiffCount == M_CL_maxHiddenInlineDiffCount) {
      showAll.style.display = "inline";
      hideAll.style.display = "none";
    } else {
      showAll.style.display = "none";
      hideAll.style.display = "inline";
    }
  }
  hide.style.display = "none";
  show.style.display = "inline";
  diffTR.style.display = "none";
  dumpDiv.style.display = "none";
  return false;
}
/**
 * Dumps the content of the given iframe into the appropriate div in order
 * for the diff to be displayed.
 * @param {Element} iframe The IFRAME that contains the diff data
 * @param {Integer} suffix The number associated with the diff
 */
function M_dumpInlineDiffContent(iframe, suffix) {
  var dumpDiv = document.getElementById("dumpDiv-" + suffix);
  dumpDiv.style.display = "block"; // XXX why not ""?
  dumpDiv.innerHTML = iframe.contentWindow.document.body.innerHTML;
  // TODO: The following should work on all browsers instead of the
  // innerHTML hack above. At this point I don't remember what the exact
  // problem was, but it didn't work for some reason.
  // dumpDiv.appendChild(iframe.contentWindow.document.body);
  if (M_isKHTML()) {
    var frameDiv = document.getElementById("frameDiv-" + suffix);
    frameDiv.style.display = "none";
  }
}
/**
 * Goes through all the diffs and triggers the onclick action on them which
 * should start the mechanism for displaying them.
 * @param {Integer} num The number of diffs to display (0-indexed)
 */
function M_showAllDiffs(num) {
  for (var i = 0; i < num; i++) {
    var link = document.getElementById('show-' + i);
    // Since the user may not have JS, the template only shows the diff inline
    // for the onclick action, not the href. In order to activate it, we must
    // call the link's onclick action.
    if (link.className.indexOf("reverted") == -1) {
      link.onclick();
    }
  }
}
/**
 * Goes through all the diffs and hides them by triggering the hide link.
 * @param {Integer} num The number of diffs to hide (0-indexed)
 */
function M_hideAllDiffs(num) {
  for (var i = 0; i < num; i++) {
    var link = document.getElementById('hide-' + i);
    // If the user tries to hide, that means they have JS, which in turn means
    // that we can just set href in the href of the hide link.
    link.onclick();
  }
}
// Inline comment submission forms (changelist.html, file.html)
/**
 * Changes the elements display style to "" which renders it visible.
 * @param {String|Element} elt The id of the element or the element itself
 */
function M_showElement(elt) {
  if (typeof elt == "string") {
    elt = document.getElementById(elt);
  }
  if (elt) elt.style.display = "";
}
/**
 * Changes the elements display style to "none" which renders it invisible.
 * @param {String|Element} elt The id of the element or the element itself
 */
function M_hideElement(elt) {
  if (typeof elt == "string") {
    elt = document.getElementById(elt);
  }
  if (elt) elt.style.display = "none";
}
/**
 * Toggle the visibility of a section. The little indicator triangle will also
 * be toggled.
 * @param {String} id The id of the target element
 */
function M_toggleSection(id) {
  var sectionStyle = document.getElementById(id).style;
  var pointerStyle = document.getElementById(id + "-pointer").style;
  if (sectionStyle.display == "none") {
    sectionStyle.display = "";
    pointerStyle.backgroundImage = "url('" + media_url + "opentriangle.gif')";
  } else {
    sectionStyle.display = "none";
    pointerStyle.backgroundImage = "url('" + media_url + "closedtriangle.gif')";
  }
}
/**
 * Callback for XMLHttpRequest.
 */
function M_PatchSetFetched() {
  if (http_request.readyState != 4)
    return;
  var section = document.getElementById(http_request.div_id);
  if (http_request.status == 200) {
    section.innerHTML = http_request.responseText;
    /* initialize dashboardState again to update cached patch rows */
    if (dashboardState) dashboardState.initialize();
  } else {
    section.innerHTML =
        '
Could not load the patchset (' +
        http_request.status + ').
';
  }
}
/**
 * Toggle the visibility of a patchset, and fetches it if necessary.
 * @param {String} issue The issue key
 * @param {String} id The patchset key
 */
function M_toggleSectionForPS(issue, patchset) {
  var id = 'ps-' + patchset;
  M_toggleSection(id);
  var section = document.getElementById(id);
  if (section.innerHTML.search("/edit_flags POST request.
 * @param {String} issue The issue key
 * @param {String} data The POST data to send in the request.
 * @param {Function} func_opt Callback called when the request completes.
 *     Take an XMLHttpRequest as argument, and returns nothing.
 */
function M_sendEditFlagsRequest(issue, data, func_opt) {
  var httpreq = M_getXMLHttpRequest();
  if (!httpreq)
    return;
  // This timeout can potentially race with the request coming back OK. In
  // general, if it hasn't come back for 60 seconds, it won't ever come back.
  var aborted = false;
  var httpreq_timeout = setTimeout(function() {
    aborted = true;
    httpreq.abort();
    alert('Request could not be updated for 60 seconds. Please ensure ' +
          'connectivity (and that the server is up) and try again.');
  }, 60000);
  httpreq.onreadystatechange = function () {
    // Firefox 2.0, at least, runs this with readyState = 4 but all other
    // fields unset when the timeout aborts the request, against all
    // documentation.
    if (httpreq.readyState == 4 && !aborted) {
      clearTimeout(httpreq_timeout);
      if (httpreq.status != 200) {
        alert('An error occurred while trying to update the issue');
      }
      if (func_opt)
        func_opt(httpreq);
    }
  }
  httpreq.open('POST', base_url + issue + '/edit_flags', true);
  httpreq.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
  httpreq.send(data);
}
/**
 * Toggle the visibility of the revert reason popup.
 */
function M_toggleRevertReasonPopup(display) {
  var popupElement = document.getElementById("revert-reason-popup-div");
  // Remove all text from the textarea while toggling.
  document.getElementById("revert_reason_textarea").value = ""
  popupElement.style.display = display ? "" : "none";
}
/**
 * Validates the revert reason and submits the revert form.
 */
function M_createRevertPatchset() {
  revert_reason = document.getElementById("revert_reason_textarea").value;
  // Validate that the revert reason is not null and does not contain only
  // newlines and whitespace characters.
  if (revert_reason == null ||
      revert_reason.replace(/(\s+|\r\n|\n|\r)/gm, "") == "") {
    alert('Must enter a revert reason. Please try again.');
    return false;
  }
  document.getElementById("revert-form")["revert_reason"].value = revert_reason;
  check_cq_value = document.getElementById("check_cq").checked ? '1' : '0';
  document.getElementById("revert-form")["revert_cq"].value = check_cq_value;
  // Confirm that this patchset should really be reverted.
  return confirm("Proceed with creating a revert of this patchset?");
}
function M_triggerCQDryRun(issue) {
  var confirmed = confirm(
      'Send this patchset to the project\'s CQ to run all of its checks without submitting the ' +
      'change.\n\nNote: The LGTM check is skipped during the dry run ' +
      'only if the user is a project committer.');
  if (!confirmed) {
    return false
  }
  // Flip the commit bit.
  var commitForm = document.getElementById("commitform");
  commitForm.commit.checked = true;
  // Flip the cq dry run bit.
  commitForm.cq_dry_run.value = true;
  // Call the server to set the necessary commit and cq_dry_run flags
  // on the issue.
  return M_editFlags(issue);
}
/**
 * Change the commit bit for the given issue by using edit_flags.
 * @param {String} issue The issue key
 */
function M_editFlags(issue) {
  var req = [];
  var len = document.commitform.elements.length;
  for (var i = 0; i < len; i++) {
    var element = document.commitform.elements[i];
    var value = undefined;
    if (element.type == 'hidden') {
      value = element.value;
    } else if (element.type == 'checkbox') {
      if (element.checked) {
        value = '1';
      } else {
        value = '0';
      }
    }
    if (value != undefined) {
      req.push(element.name + '=' + encodeURIComponent(value));
    }
  }
  M_sendEditFlagsRequest(issue, req.join("&"), function(xhr) {
    if (xhr.status == 200)
      window.location.reload();
  });
  return true;
}
/**
 * Edit the list of pending try jobs for the given patchset. 
 * @param {String} patchset The patchset key.
 */
function M_editPendingTryJobs(patchset) {
  var checkboxContainer = document.getElementById('trybot-popup-checkboxes');
  if (! checkboxContainer) {
    checkboxContainer = document.getElementById(
        'trybot-popup-checkboxes-with-categories')
  }
  var checkboxElements = checkboxContainer.getElementsByTagName('input');
  for (var checkbox, i = 0; checkbox = checkboxElements[i]; i++) {
    checkbox.checked = false;
    checkbox.disabled = false;
  }
  // Position popup below anchor.
  var anchor = document.getElementById('tryjobchange-' + patchset);
  var anchorRect = anchor.getBoundingClientRect();
  var popupElement = document.getElementById('trybot-popup');
  popupElement.style.left = anchorRect.left + 'px';
  popupElement.style.top = anchorRect.bottom + 'px';
  popupElement.style.display = '';
  // Extra padding to allow for scrollbars.
  var scrollbarWidth = 20;
  // Move popup as need to be on screen.
  var popupRect = popupElement.getBoundingClientRect();
  if (popupRect.bottom > window.innerHeight)
    popupElement.style.top = (anchorRect.bottom - (popupRect.bottom - window.innerHeight) - scrollbarWidth) + 'px';  
  if (popupRect.right > window.innerWidth)
    popupElement.style.left = (anchorRect.left - (popupRect.right - window.innerWidth) - scrollbarWidth) + 'px';  
}
/**
 * Updates the pending builders for the patchset.
 * @param {String} issue The issue key.
 * @param {String} patchset The patchset key.
 * @param {String} xsrf_token Security token.
 */
function M_updatePendingTrybots(issue, patchset, xsrf_token) {
  // Find which builder are checked.
  var builders = [];
  var popup = jQuery('#trybot-popup');
  jQuery('input:checkbox', popup).each(function(i) {
    var self = jQuery(this);
    if (self.attr('checked'))
      builders.push(self.attr('name')); 
  });
  
  // Build POST data for request.
  var data = [];
  data.push('xsrf_token=' + xsrf_token);
  data.push('last_patchset=' + patchset);
  data.push('builders=' + builders.join(','));
  
  M_sendEditFlagsRequest(issue, data.join("&"), function(xhr) {
    if (xhr.status == 200)
      window.location.reload();
  });
  
  // Hide the popup.
  jQuery('#trybot-popup').css('display', 'none');
  return true;
}
/**
 * Hide the pending builders popup.
 */
function M_closePendingTrybots() {
  jQuery('#trybot-popup').css('display', 'none');
}
/**
 * Show or hide older try bot results. 
 * @param {String} id The id of the div elements that holds all the try job
 *     a elements.
 * @param makeVisible If true, makes older try bots visible.
 */
function M_showTryJobResult(id, makeVisible) {
  // This set keeps track of the first occurance of each try job result for
  // a given builder.  The try job results are ordered reverse chronologically,
  // so we visit them from newest to oldest.
  var firstBuilderSet = {};
  var oldBuildersExist = false;
  jQuery('a', document.getElementById(id)).each(function(i) {
    var self = jQuery(this);
    var builder = self.text();
    if (self.attr('category') != 'cq_experimental')
    {
      if (self.attr('status') == 'try-pending') {
        // Try pending jobs are always visible.
        self.css('display', 'inline');
      } else if (builder in firstBuilderSet) {
        // This is not the first time we see this builder, so toggle its
        // visibility.
        self.css('display', makeVisible ? 'inline' : 'none');
        oldBuildersExist = true;
      } else {
        // The first time we see a builder, its always visible.  Remember the
        // builder name.
        self.css('display', 'inline');
        firstBuilderSet[builder] = true;
      }
    }
  });
  jQuery('#' + id + '-morelink')
    .css('display', !oldBuildersExist || makeVisible ? 'none' : '');
  jQuery('#' + id + '-lesslink')
    .css('display', oldBuildersExist && makeVisible ? '' : 'none');
}
/**
 * Toggle the visibility of the "Quick LGTM" link on the changelist page.
 * @param {String} id The id of the target element
 */
function M_toggleQuickLGTM(id) {
  M_toggleSection(id);
  window.scrollTo(0, document.body.offsetHeight);
}
// Comment expand/collapse
/**
 * Toggles whether the specified changelist comment is expanded/collapsed.
 * @param {Integer} cid The comment id, 0-indexed
 */
function M_switchChangelistComment(cid) {
  M_switchCommentCommon_('cl', String(cid));
}
/**
 * Toggles a comment or patchset.
 *
 * If the anchor has the form "#msgNUMBER" a message is toggled.
 * If the anchor has the form "#psNUMBER" a patchset is toggled.
 */
function M_toggleIssueOverviewByAnchor() {
  var href = window.location.href;
  var idx_hash = href.lastIndexOf('#');
  if (idx_hash != -1) {
    var anchor = href.slice(idx_hash+1, href.length);
    if (anchor.slice(0, 3) == 'msg') {
      var elem = document.getElementById(anchor);
      elem.className += ' referenced';
      var num = elem.getAttribute('name');
      if (anchor.slice(3) != lastMsgID)
        M_switchChangelistComment(num);
    } else if (anchor.slice(0, 2) == 'ps') {
      // hide last patchset which is visible by default.
      M_toggleSectionForPS(issueId, lastPSId);
      M_toggleSectionForPS(issueId, anchor.slice(2, anchor.length));
    }
  }
}
/**
 * Toggles whether the specified inline comment is expanded/collapsed.
 * @param {Integer} cid The comment id, 0-indexed
 * @param {Integer} lineno The lineno associated with the comment
 * @param {String} side The side (a/b) associated with the comment
 */
function M_switchInlineComment(cid, lineno, side) {
  M_switchCommentCommon_('inline', String(cid) + "-" + lineno + "-" + side);
}
/**
 * Used to expand all visible comments, hiding the preview and showing the
 * comment.
 * @param {String} prefix The level of the comment -- one of
 *                        ('cl', 'file', 'inline')
 * @param {Integer} num_comments The number of comments to show
 */
function M_expandAllVisibleComments(prefix, num_comments) {
  for (var i = 0; i < num_comments; i++) {
    M_hideElement(prefix + "-preview-" + i);
    M_showElement(prefix + "-comment-" + i);
  }
}
/**
 * Used to collapse all visible comments, showing the preview and hiding the
 * comment.
 * @param {String} prefix The level of the comment -- one of
 *                        ('cl', 'file', 'inline')
 * @param {Integer} num_comments The number of comments to hide
 */
function M_collapseAllVisibleComments(prefix, num_comments) {
  for (var i = 0; i < num_comments; i++) {
    M_showElement(prefix + "-preview-" + i);
    M_hideElement(prefix + "-comment-" + i);
  }
}
/**
 * Used to show all auto_generated comments.
 * @param {Integer} num_comments The total number of comments to loop through
 */
function M_showGeneratedComments(num_comments) {
  for (var i = 0; i < num_comments; i++) {
    // The top level msg div starts at index 1.
    M_showElement("generated-msg" + (i+1));
  }
}
/**
 * Used to hide all auto_generated comments.
 * @param {Integer} num_comments The total number of comments to loop through
 */
function M_hideGeneratedComments(num_comments) {
  for (var i = 0; i < num_comments; i++) {
    // The top level msg div starts at index 1.
    M_hideElement("generated-msg" + (i+1));
  }
}
// Common methods for comment handling (changelist.html, file.html,
// comment_form.html)
/**
 * Toggles whether the specified comment is expanded/collapsed. Works in
 * the review form.
 * @param {String} prefix The prefix of the comment element name.
 * @param {String} suffix The suffix of the comment element name.
 */
function M_switchCommentCommon_(prefix, suffix) {
  prefix && (prefix +=  '-');
  suffix && (suffix =  '-' + suffix);
  var previewSpan = document.getElementById(prefix + 'preview' + suffix);
  var commentDiv = document.getElementById(prefix + 'comment' + suffix);
  if (!previewSpan || !commentDiv) {
    alert('Failed to find comment element: ' +
          prefix + 'comment' + suffix + '. Please send ' +
          'this message with the URL to the app owner');
    return;
  }
  if (previewSpan.style.display == 'none') {
    M_showElement(previewSpan);
    M_hideElement(commentDiv);
  } else {
    M_hideElement(previewSpan);
    M_showElement(commentDiv);
  }
}
/**
 * Expands all inline comments.
 */
function M_expandAllInlineComments() {
  M_showAllInlineComments();
  var comments = document.getElementsByName("inline-comment");
  var commentsLength = comments.length;
  for (var i = 0; i < commentsLength; i++) {
    comments[i].style.display = "";
  }
  var previews = document.getElementsByName("inline-preview");
  var previewsLength = previews.length;
  for (var i = 0; i < previewsLength; i++) {
    previews[i].style.display = "none";
  }
}
/**
 * Collapses all inline comments.
 */
function M_collapseAllInlineComments() {
  M_showAllInlineComments();
  var comments = document.getElementsByName("inline-comment");
  var commentsLength = comments.length;
  for (var i = 0; i < commentsLength; i++) {
    comments[i].style.display = "none";
  }
  var previews = document.getElementsByName("inline-preview");
  var previewsLength = previews.length;
  for (var i = 0; i < previewsLength; i++) {
    previews[i].style.display = "";
  }
}
// Non-inline comment actions
/**
 * Sets up a reply form for a given comment (non-inline).
 * @param {String} author The author of the comment being replied to
 * @param {String} written_time The formatted time when that comment was written
 * @param {String} ccs A string containing the ccs to default to
 * @param {Integer} cid The number of the comment being replied to, so that the
 *                      form may be placed in the appropriate location
 * @param {String} prefix The level of the comment -- one of
 *                        ('cl', 'file', 'inline')
 * @param {Integer} opt_lineno (optional) The line number the comment should be
 *                                        attached to
 * @param {String} opt_snapshot (optional) The snapshot ID of the comment being
 *                                         replied to
 */
function M_replyToComment(author, written_time, ccs, cid, prefix, opt_lineno,
                          opt_snapshot) {
  var form = document.getElementById("comment-form-" + cid);
  if (!form) {
    form = document.getElementById("dareplyform");
    if (!form) {
      form = document.getElementById("daform"); // XXX for file.html
    }
    form = form.cloneNode(true);
    form.name = form.id = "comment-form-" + cid;
    M_createResizer_(form, cid);
    document.getElementById(prefix + "-comment-" + cid).appendChild(form);
  }
  form.style.display = "";
  form.reply_to.value = cid;
  form.ccs.value = ccs;
  if (typeof opt_lineno != 'undefined' && typeof opt_snapshot != 'undefined') {
    form.lineno.value = opt_lineno;
    form.snapshot.value = opt_snapshot;
  }
  form.text.value = "On " + written_time + ", " + author + " wrote:\n";
  var divs = document.getElementsByName("comment-text-" + cid);
  M_setValueFromDivs(divs, form.text);
  form.text.value += "\n";
  form.text.focus();
}
/**
/* TODO(andi): docstring
 */
function M_replyToMessage(message_id, written_time, author,
                          db_message_object_key) {
  var form = document.getElementById('message-reply-form');
  form = form.cloneNode(true);
  var container = document.getElementById('message-reply-'+message_id);
  var replyLink = document.getElementById('message-reply-href-'+message_id);
  var msgTextarea = replyLink.nextSibling.nextSibling;
  form.insertBefore(msgTextarea, form.firstChild);
  M_showElement(msgTextarea);
  container.appendChild(form);
  M_showElement(container);
  form.in_reply_to.value = db_message_object_key;
  form.discard.onclick = function () {
    form.message.value = "";
    M_getParent(container).insertBefore(msgTextarea, replyLink.nextSibling.nextSibling);
    M_showElement(replyLink);
    M_hideElement(msgTextarea);
    container.innerHTML = "";
  }
  if (!form.message.value) {
    form.message.value = "On " + written_time + ", " + author + " wrote:\n";
    var divs = document.getElementsByName("cl-message-" + message_id);
    form.message.focus();
    M_setValueFromDivs(divs, form.message);
    form.message.value += "\n";
  }
  // Scroll view to bottom of message textarea and scroll textarea to bottom
  // too, so that the user can write w/o adjusting the views first.
  M_scrollIntoView(window, form.send_mail, 1);
  form.message.scrollTop = form.message.scrollHeight;
  M_addTextResizer_(form);
  M_hideElement(replyLink);
}
/**
 * Edits a non-inline draft comment.
 * @param {Integer} cid The number of the comment to be edited
 */
function M_editComment(cid) {
  var suffix = String(cid);
  var form = document.getElementById("comment-form-" + suffix);
  if (!form) {
    alert("Form " + suffix + " does not exist. Please send this message " +
          "with the URL to the app owner");
    return false;
  }
  var texts = document.getElementsByName("comment-text-" + suffix);
  var textsLength = texts.length;
  for (var i = 0; i < textsLength; i++) {
    texts[i].style.display = "none";
  }
  M_hideElement("edit-link-" + suffix);
  M_hideElement("undo-link-" + suffix);
  form.style.display = "";
  form.text.focus();
}
/**
 * Used to cancel comment editing, this will revert the text of the comment
 * and hide its form.
 * @param {Element} form The form that contains this comment
 * @param {Integer} cid The number of the comment being hidden
 */
function M_resetAndHideComment(form, cid) {
  form.text.blur();
  form.text.value = form.oldtext.value;
  form.style.display = "none";
  var texts = document.getElementsByName("comment-text-" + cid);
  var textsLength = texts.length;
  for (var i = 0; i < textsLength; i++) {
    texts[i].style.display = "";
  }
  M_showElement("edit-link-" + cid);
}
/**
 * Removing a draft comment is the same as setting its text contents to nothing.
 * @param {Element} form The form containing the draft comment to be discarded
 * @return true in order for the form submission to continue
 */
function M_removeComment(form) {
  form.text.value = "";
  return true;
}
// Inline comments (file.html)
/**
 * Helper method to assign an onclick handler to an inline 'Cancel' button.
 * @param {Element} form The form containing the cancel button
 * @param {Function} cancelHandler A function with one 'form' argument
 * @param {Array} opt_handlerParams An array whose first three elements are:
 *   {String} cid The number of the comment
 *   {String} lineno The line number of the comment
 *   {String} side 'a' or 'b'
 */
function M_assignToCancel_(form, cancelHandler, opt_handlerParams) {
  var elementsLength = form.elements.length;
  for (var i = 0; i < elementsLength; ++i) {
    if (form.elements[i].getAttribute("name") == "cancel") {
      form.elements[i].onclick = function() {
        if (typeof opt_handlerParams != "undefined") {
          var cid = opt_handlerParams[0];
          var lineno = opt_handlerParams[1];
          var side = opt_handlerParams[2];
          cancelHandler(form, cid, lineno, side);
        } else {
          cancelHandler(form);
        }
      };
      return;
    }
  }
}
/**
 * Helper method to assign an onclick handler to an inline '[+]' link.
 * @param {Element} form The form containing the resizer
 * @param {String} suffix The suffix of the comment form id: lineno-side
 */
function M_createResizer_(form, suffix) {
  if (!form.hasResizer) {
    var resizer = document.getElementById("resizer").cloneNode(true);
    resizer.onclick = function() {
      var form = document.getElementById("comment-form-" + suffix);
      if (!form) return;
      form.text.rows += 5;
      form.text.focus();
    };
    var elementsLength = form.elements.length;
    for (var i = 0; i < elementsLength; ++i) {
      var node = form.elements[i];
      if (node.nodeName == "TEXTAREA") {
        var parent = M_getParent(node);
        parent.insertBefore(resizer, node.nextSibling);
        resizer.style.display = "";
        form.hasResizer = true;
      }
    }
  }
}
/**
 * Like M_createResizer_(), but updates the form's first textarea field.
 * This is assumed not to be the last field.
 * @param {Element} form The form whose textarea field to update.
 */
function M_addTextResizer_(form) {
  if (M_isWebKit()) {
    return; // WebKit has its own resizer.
  }
  var elementsLength = form.elements.length;
  for (var i = 0; i < elementsLength; ++i) {
    var node = form.elements[i];
    if (node.nodeName == "TEXTAREA") {
      var parent = M_getParent(node);
      var resizer = document.getElementById("resizer").cloneNode(true);
      var next = node.nextSibling;
      parent.insertBefore(resizer, next);
      resizer.onclick = function() {
	node.rows += 5;
	node.focus();
      };
      resizer.style.display = "";
      if (next && next.className == "resizer") { // Remove old resizer.
	parent.removeChild(next);
      }
      break;
    }
  }
}
/**
 * Helper method to assign an onclick handler to an inline 'Save' button.
 * @param {Element} form The form containing the save button
 * @param {String} cid The number of the comment
 * @param {String} lineno The line number of the comment
 * @param {String} side 'a' or 'b'
 */
function M_assignToSave_(form, cid, lineno, side) {
  var elementsLength = form.elements.length;
  for (var i = 0; i < elementsLength; ++i) {
    if (form.elements[i].getAttribute("name") == "save") {
      form.elements[i].onclick = function() {
        return M_submitInlineComment(form, cid, lineno, side);
      };
      return;
    }
  }
}
/**
 * Creates an inline comment at the given line number and side of the diff.
 * @param {String} lineno The line number of the new comment
 * @param {String} side Either 'a' or 'b' signifying the side of the diff
 */
function M_createInlineComment(lineno, side) {
  // The first field of the suffix is typically the cid, but we choose '-1'
  // here since the backend has not assigned the new comment a cid yet.
  var suffix = "-1-" + lineno + "-" + side;
  var form = document.getElementById("comment-form-" + suffix);
  if (!form) {
    form = document.getElementById("dainlineform").cloneNode(true);
    form.name = form.id = "comment-form-" + suffix;
    M_assignToCancel_(form, M_removeTempInlineComment);
    M_createResizer_(form, suffix);
    M_assignToSave_(form, "-1", lineno, side);
    // There is a "text" node before the "div" node
    form.childNodes[1].setAttribute("name", "comment-border");
    var id = (side == 'a' ? "old" : "new") + "-line-" + lineno;
    var td = document.getElementById(id);
    td.appendChild(form);
    var tr = M_getParent(td);
    tr.setAttribute("name", "hook");
    hookState.updateHooks();
  }
  form.style.display = "";
  form.lineno.value = lineno;
  if (side == 'b') {
    form.snapshot.value = new_snapshot;
  } else {
    form.snapshot.value = old_snapshot;
  }
  form.side.value = side;
  var savedDraftKey = "new-" + form.lineno.value + "-" + form.snapshot.value;
  M_restoreDraftText_(savedDraftKey, form);
  form.text.focus();
  hookState.gotoHook(0);
}
/**
 * Removes a never-submitted 'Reply' inline comment from existence (created via
 * M_replyToInlineComment).
 * @param {Element} form The form that contains the comment to be removed
 * @param {String} cid The number of the comment
 * @param {String} lineno The line number of the comment
 * @param {String} side 'a' or 'b'
 */
function M_removeTempReplyInlineComment(form, cid, lineno, side) {
  var divInlineComment = M_getParent(form);
  var divCommentBorder = M_getParent(divInlineComment);
  var td = M_getParent(divCommentBorder);
  var tr = M_getParent(td);
  form.cancel.blur();
  // The order of the subsequent lines is sensitive to browser compatibility.
  var suffix = cid + "-" + lineno + "-" + side;
  M_saveDraftText_("reply-" + suffix, form.text.value);
  divInlineComment.removeChild(form);
  M_updateRowHook(tr);
}
/**
 * Removes a never-submitted inline comment from existence (created via
 * M_createInlineComment). Saves the existing text for the next time a draft is
 * created on the same line.
 * @param {Element} form The form that contains the comment to be removed
 */
function M_removeTempInlineComment(form) {
  var td = M_getParent(form);
  var tr = M_getParent(td);
  // The order of the subsequent lines is sensitive to browser compatibility.
  var savedDraftKey = "new-" + form.lineno.value + "-" + form.snapshot.value;
  M_saveDraftText_(savedDraftKey, form.text.value);
  form.cancel.blur();
  td.removeChild(form);
  M_updateRowHook(tr);
}
/**
 * Helper to edit a draft inline comment.
 * @param {String} cid The number of the comment
 * @param {String} lineno The line number of the comment
 * @param {String} side 'a' or 'b'
 * @return {Element} The form that contains the comment
 */
function M_editInlineCommentCommon_(cid, lineno, side) {
  var suffix = cid + "-" + lineno + "-" + side;
  var form = document.getElementById("comment-form-" + suffix);
  if (!form) {
    alert("Form " + suffix + " does not exist. Please send this message " +
          "with the URL to the app owner");
    return false;
  }
  M_createResizer_(form, suffix);
  var texts = document.getElementsByName("comment-text-" + suffix);
  var textsLength = texts.length;
  for (var i = 0; i < textsLength; i++) {
    texts[i].style.display = "none";
  }
  var hides = document.getElementsByName("comment-hide-" + suffix);
  var hidesLength = hides.length;
  for (var i = 0; i < hidesLength; i++) {
    hides[i].style.display = "none";
    var links = hides[i].getElementsByTagName("A");
    if (links && links.length > 0) {
      var link = links[0];
      link.innerHTML = "Show quoted text";
    }
  }
  M_hideElement("edit-link-" + suffix);
  M_hideElement("undo-link-" + suffix);
  form.style.display = "";
  var parent = document.getElementById("inline-comment-" + suffix);
  if (parent && parent.style.display == "none") {
    M_switchInlineComment(cid, lineno, side);
  }
  form.text.focus();
  hookState.gotoHook(0);
  return form;
}
/**
 * Edits a draft inline comment.
 * @param {String} cid The number of the comment
 * @param {String} lineno The line number of the comment
 * @param {String} side 'a' or 'b'
 */
function M_editInlineComment(cid, lineno, side) {
  M_editInlineCommentCommon_(cid, lineno, side);
}
/**
 * Restores a canceled draft inline comment for editing.
 * @param {String} cid The number of the comment
 * @param {String} lineno The line number of the comment
 * @param {String} side 'a' or 'b'
 */
function M_restoreEditInlineComment(cid, lineno, side) {
  var form = M_editInlineCommentCommon_(cid, lineno, side);
  var savedDraftKey = "edit-" + cid + "-" + lineno + "-" + side;
  M_restoreDraftText_(savedDraftKey, form, false);
}
/**
 * Helper to reply to an inline comment.
 * @param {String} author The author of the comment being replied to
 * @param {String} written_time The formatted time when that comment was written
 * @param {String} ccs A string containing the ccs to default to
 * @param {String} cid The number of the comment being replied to, so that the
 *                     form may be placed in the appropriate location
 * @param {String} lineno The line number of the comment
 * @param {String} side 'a' or 'b'
 * @param {String} opt_reply The response to pre-fill with.
 * @param {Boolean} opt_submit This will submit the comment right after
 *                             creation. Only makes sense when opt_reply is set
 * @return {Element} The form that contains the comment
 */
function M_replyToInlineCommentCommon_(author, written_time, cid, lineno,
                                       side, opt_reply, opt_submit) {
  var suffix = cid + "-" + lineno + "-" + side;
  var form = document.getElementById("comment-form-" + suffix);
  if (!form) {
    form = document.getElementById("dainlineform").cloneNode(true);
    form.name = form.id = "comment-form-" + suffix;
    M_assignToCancel_(form, M_removeTempReplyInlineComment,
                      [cid, lineno, side]);
    M_assignToSave_(form, cid, lineno, side);
    M_createResizer_(form, suffix);
    var parent = document.getElementById("inline-comment-" + suffix);
    if (parent.style.display == "none") {
      M_switchInlineComment(cid, lineno, side);
    }
    parent.appendChild(form);
  }
  form.style.display = "";
  form.lineno.value = lineno;
  if (side == 'b') {
    form.snapshot.value = new_snapshot;
  } else {
    form.snapshot.value = old_snapshot;
  }
  form.side.value = side;
  if (!M_restoreDraftText_("reply-" + suffix, form, false) ||
      typeof opt_reply != "undefined") {
    form.text.value = "On " + written_time + ", " + author + " wrote:\n";
    var divs = document.getElementsByName("comment-text-" + suffix);
    M_setValueFromDivs(divs, form.text);
    form.text.value += "\n";
    if (typeof opt_reply != "undefined") {
      form.text.value += opt_reply;
    }
    if (opt_submit) {
      M_submitInlineComment(form, cid, lineno, side);
      return;
    }
  }
  form.text.focus();
  hookState.gotoHook(0);
  return form;
}
/**
 * Replies to an inline comment.
 * @param {String} author The author of the comment being replied to
 * @param {String} written_time The formatted time when that comment was written
 * @param {String} ccs A string containing the ccs to default to
 * @param {String} cid The number of the comment being replied to, so that the
 *                     form may be placed in the appropriate location
 * @param {String} lineno The line number of the comment
 * @param {String} side 'a' or 'b'
 * @param {String} opt_reply The response to pre-fill with.
 * @param {Boolean} opt_submit This will submit the comment right after
 *                             creation. Only makes sense when opt_reply is set
 */
function M_replyToInlineComment(author, written_time, cid, lineno, side,
                                opt_reply, opt_submit) {
  M_replyToInlineCommentCommon_(author, written_time, cid, lineno, side,
                                opt_reply, opt_submit);
}
/**
 * Restores a canceled draft inline comment for reply.
 * @param {String} author The author of the comment being replied to
 * @param {String} written_time The formatted time when that comment was written
 * @param {String} ccs A string containing the ccs to default to
 * @param {String} cid The number of the comment being replied to, so that the
 *                     form may be placed in the appropriate location
 * @param {String} lineno The line number of the comment
 * @param {String} side 'a' or 'b'
 */
function M_restoreReplyInlineComment(author, written_time, cid, lineno,
                                     side) {
  var form = M_replyToInlineCommentCommon_(author, written_time, cid,
                                           lineno, side);
  var savedDraftKey = "reply-" + cid + "-" + lineno + "-" + side;
  M_restoreDraftText_(savedDraftKey, form, false);
}
/**
 * Updates an inline comment td with the given HTML.
 * @param {Element} td The TD that contains the inline comment
 * @param {String} html The text to be put into .innerHTML of the td
 */
function M_updateInlineComment(td, html) {
  var tr = M_getParent(td);
  if (!tr) {
    alert("TD had no parent. Please notify the app owner.");
    return;
  }
  // The server sends back " " to make things empty, for Safari
  if (html.length <= 1) {
    td.innerHTML = "";
    M_updateRowHook(tr);
  } else {
    td.innerHTML = html;
    tr.name = "hook";
    hookState.updateHooks();
  }
}
/**
 * Updates a comment tr's name, depending on whether there are now comments
 * in it or not. Also updates the hook cache if required. Assumes that the
 * given TR already has name == "hook" and only tries to remove it if all
 * are empty.
 * @param {Element} tr The TR containing the potential comments
 */
function M_updateRowHook(tr) {
  if (!(tr && tr.cells)) return;
  // If all of the TR's cells are empty, remove the hook name
  var i = 0;
  var numCells = tr.cells.length;
  for (i = 0; i < numCells; i++) {
    if (tr.cells[i].innerHTML != "") {
      break;
    }
  }
  if (i == numCells) {
    tr.setAttribute("name",  "");
    hookState.updateHooks();
  }
  hookState.gotoHook(0);
}
/**
 * Submits an inline comment and updates the DOM in AJAX fashion with the new
 * comment data for that line.
 * @param {Element} form The form containing the submitting comment
 * @param {String} cid The number of the comment
 * @param {String} lineno The line number of the comment
 * @param {String} side 'a' or 'b'
 * @return true if AJAX fails and the form should be submitted the "old" way,
 *         or false if the form is submitted using AJAX, preventing the regular
 *         form submission from proceeding
 */
function M_submitInlineComment(form, cid, lineno, side) {
  var td = null;
  if (form.side.value == 'a') {
    td = document.getElementById("old-line-" + form.lineno.value);
  } else {
    td = document.getElementById("new-line-" + form.lineno.value);
  }
  if (!td) {
    alert("Could not find snapshot " + form.snapshot.value + "! Please let " +
          "the app owner know.");
    return true;
  }
  if (typeof side == "undefined") {
    side = form.side.value;
  }
  // Clear saved draft state for affected new, edited, and replied comments
  if (typeof cid != "undefined" && typeof lineno != "undefined" && side) {
    var suffix = cid + "-" + lineno + "-" + side;
    M_clearDraftText_("new-" + lineno + "-" + form.snapshot.value);
    M_clearDraftText_("edit-" + suffix);
    M_clearDraftText_("reply-" + suffix);
    M_hideElement("undo-link-" + suffix);
  }
  var httpreq = M_getXMLHttpRequest();
  if (!httpreq) {
    // No AJAX. Oh well. Go ahead and submit this the old way.
    return true;
  }
  // Konqueror jumps to a random location for some reason
  var scrollTop = M_getScrollTop(window);
  var aborted = false;
  reenable_form = function() {
    form.save.disabled = false;
    form.cancel.disabled = false;
    if (form.discard != null) {
      form.discard.disabled = false;
    }
    form.text.disabled = false;
    form.style.cursor = "auto";
  };
  // This timeout can potentially race with the request coming back OK. In
  // general, if it hasn't come back for 60 seconds, it won't ever come back.
  var httpreq_timeout = setTimeout(function() {
    aborted = true;
    httpreq.abort();
    reenable_form();
    alert("Comment could not be submitted for 60 seconds. Please ensure " +
          "connectivity (and that the server is up) and try again.");
  }, 60000);
  httpreq.onreadystatechange = function () {
    // Firefox 2.0, at least, runs this with readyState = 4 but all other
    // fields unset when the timeout aborts the request, against all
    // documentation.
    if (httpreq.readyState == 4 && !aborted) {
      clearTimeout(httpreq_timeout);
      if (httpreq.status == 200) {
        M_updateInlineComment(td, httpreq.responseText);
      } else {
        reenable_form();
        alert("An error occurred while trying to submit the comment: " +
              httpreq.statusText);
      }
      if (M_isKHTML()) {
        window.scrollTo(0, scrollTop);
      }
    }
  }
  httpreq.open("POST", base_url + "inline_draft", true);
  httpreq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
  var req = [];
  var len = form.elements.length;
  for (var i = 0; i < len; i++) {
    var element = form.elements[i];
    if (element.type == "hidden" || element.type == "textarea") {
      req.push(element.name + "=" + encodeURIComponent(element.value));
    }
  }
  req.push("side=" + side);
  // Disable forever. If this succeeds, then the form will end up getting
  // rewritten, and if it fails, the page should get a refresh anyways.
  form.save.blur();
  form.save.disabled = true;
  form.cancel.blur();
  form.cancel.disabled = true;
  if (form.discard != null) {
    form.discard.blur();
    form.discard.disabled = true;
  }
  form.text.blur();
  form.text.disabled = true;
  form.style.cursor = "wait";
  // Send the request
  httpreq.send(req.join("&"));
  // No need to resubmit this form.
  return false;
}
/**
 * Removes a draft inline comment.
 * @param {Element} form The form that contains the comment to be removed
 * @param {String} cid The number of the comment
 * @param {String} lineno The line number of the comment
 * @param {String} side 'a' or 'b'
 */
function M_removeInlineComment(form, cid, lineno, side) {
  // Update state to save the canceled edit text
  var snapshot = side == "a" ? old_snapshot : new_snapshot;
  var savedDraftKey = "new-" + lineno + "-" + snapshot;
  var savedText = form.text.value;
  form.text.value = "";
  var ret = M_submitInlineComment(form, cid, lineno, side);
  M_saveDraftText_(savedDraftKey, savedText);
  return ret;
}
/**
 * Combines all the divs from a single comment (generated by multiple buckets)
 * and undoes the escaping work done by Django filters, and inserts the result
 * into a given textarea.
 * @param {Array} divs An array of div elements to be combined
 * @param {Element} text The textarea whose value needs to be updated
 */
function M_setValueFromDivs(divs, text) {
  var lines = [];
  var divsLength = divs.length;
  for (var i = 0; i < divsLength; i++) {
    lines = lines.concat(divs[i].innerHTML.split("\n"));
    // It's _fairly_ certain that the last line in the div will be
    // empty, based on how the template works. If the last line in the
    // array is empty, then ignore it.
    if (lines.length > 0 && lines[lines.length - 1] == "") {
      lines.length = lines.length - 1;
    }
  }
  for (var i = 0; i < lines.length; i++) {
    // Undo the 
 tags added by urlize and urlizetrunc
    lines[i] = lines[i].replace(/(.*?)<\/a>/ig, '$2');
    // Undo the escape Django filter
    lines[i] = lines[i].replace(/>/ig, ">");
    lines[i] = lines[i].replace(/</ig, "<");
    lines[i] = lines[i].replace(/"/ig, "\"");
    lines[i] = lines[i].replace(/'/ig, "'");
    lines[i] = lines[i].replace(/&/ig, "&"); // Must be last
    text.value += "> " + lines[i] + "\n";
  }
}
/**
 * Return the specified URL parameter.
 * @param {String} sParam The name of the parameter.
 */
function M_getUrlParameter(sParam) {
    var sPageURL = window.location.search.substring(1);
    var sURLVariables = sPageURL.split('&');
    for (var i = 0; i < sURLVariables.length; i++) {
        var sParameterName = sURLVariables[i].split('=');
        if (sParameterName[0] == sParam) {
            return sParameterName[1];
        }
    }
}
/**
 * Undo an edit of a draft inline comment, i.e. discard changes.
 * @param {Element} form The form containing the edits
 * @param {String} cid The number of the comment
 * @param {String} lineno The line number of the comment
 * @param {String} side 'a' or 'b'
 */
function M_resetAndHideInlineComment(form, cid, lineno, side) {
  // Update canceled edit state
  var suffix = cid + "-" + lineno + "-" + side;
  M_saveDraftText_("edit-" + suffix, form.text.value);
  if (form.text.value != form.oldtext.value) {
    M_showElement("undo-link-" + suffix);
  }
  form.text.blur();
  form.text.value = form.oldtext.value;
  form.style.display = "none";
  var texts = document.getElementsByName("comment-text-" + suffix);
  var textsLength = texts.length;
  for (var i = 0; i < textsLength; i++) {
    if (texts[i].className.indexOf("comment-text-quoted") < 0) {
      texts[i].style.display = "";
    }
  }
  var hides = document.getElementsByName("comment-hide-" + suffix);
  var hidesLength = hides.length;
  for (var i = 0; i < hidesLength; i++) {
    hides[i].style.display = "";
  }
  M_showElement("edit-link-" + suffix);
  hookState.gotoHook(0);
}
/**
 * Toggles whether we display quoted text or not, both for inline and regular
 * comments. Inline comments will have lineno and side defined.
 * @param {String} cid The comment number
 * @param {String} bid The bucket number in that comment
 * @param {String} lineno (optional) Line number of the comment
 * @param {String} side (optional) 'a' or 'b'
 */
function M_switchQuotedText(cid, bid, lineno, side) {
  var tmp = ""
  if (typeof lineno != 'undefined' && typeof side != 'undefined')
    tmp = "-" + lineno + "-" + side;
  var extra = cid + tmp + "-" + bid;
  var div = document.getElementById("comment-text-" + extra);
  var a = document.getElementById("comment-hide-link-" + extra);
  if (div.style.display == "none") {
    div.style.display = "";
    a.innerHTML = "Hide quoted text";
  } else {
    div.style.display = "none";
    a.innerHTML = "Show quoted text";
  }
  if (tmp != "") {
    hookState.gotoHook(0);
  }
}
/**
 * Handler for the double click event in the code table element. Creates a new
 * inline comment for that line of code on the right side of the diff.
 * @param {Event} evt The event object for this double-click event
 */
function M_handleTableDblClick(evt) {
  if (!logged_in) {
    if (!login_warned) {
      login_warned = true;
      alert("Please sign in to enter inline comments.");
    }
    return;
  }
  var evt = evt ? evt : (event ? event : null);
  var target = M_getEventTarget(evt);
  if (target.tagName == 'INPUT' || target.tagName == 'TEXTAREA') {
    return;
  }
  while (target != null && target.tagName != 'TD') {
    target = M_getParent(target);
  }
  if (target == null) {
    return;
  }
  var side = null;
  if (target.id.substr(0, 7) == "newcode") {
    side = 'b';
  } else if (target.id.substr(0, 7) == "oldcode") {
    side = 'a';
  }
  if (side != null) {
    M_createInlineComment(parseInt(target.id.substr(7)), side);
  }
}
var M_timerLongTap = null;
/**
 * Resets the long tap timer iff activated.
 */
function M_clearTableTouchTimeout() {
  if (M_timerLongTap) {
    clearTimeout(M_timerLongTap);
  }
  M_timerLongTap = null;
}
/**
 * Handles long tap events on mobile devices (touchstart).
 *
 * This function activates a 1sec timeout that redirects the event to
 * M_handleTableDblClick().
 */
function M_handleTableTouchStart(evt) {
  if (evt.touches && evt.touches.length == 1) { // 1 finger touch
    M_clearTableTouchTimeout();
    M_timerLongTap = setTimeout(function() {
      M_clearTableTouchTimeout();
      M_handleTableDblClick(evt);
    }, 1000);
  }
}
function M_handleTableTouchMove(evt) {
  M_clearTableTouchTimeout();
}
/**
 * Handles touchend event for long taps on mobile devices.
 */
function M_handleTableTouchEnd(evt) {
  M_clearTableTouchTimeout();
}
/**
 * Makes all inline comments visible. This is the default view.
 */
function M_showAllInlineComments() {
  var hide_elements = document.getElementsByName("hide-all-inline");
  var show_elements = document.getElementsByName("show-all-inline");
  for (var i = 0; i < hide_elements.length; i++) {
    hide_elements[i].style.display = "";
  }
  var elements = document.getElementsByName("comment-border");
  var elementsLength = elements.length;
  for (var i = 0; i < elementsLength; i++) {
    var tr = M_getParent(M_getParent(elements[i]));
    tr.style.display = "";
    tr.name = "hook";
  }
  for (var i = 0; i < show_elements.length; i++) {
    show_elements[i].style.display = "none";
  }
  hookState.updateHooks();
}
/**
 * Hides all inline comments, to make code easier ot read.
 */
function M_hideAllInlineComments() {
  var hide_elements = document.getElementsByName("hide-all-inline");
  var show_elements = document.getElementsByName("show-all-inline");
  for (var i = 0; i < show_elements.length; i++) {
    show_elements[i].style.display = "";
  }
  var elements = document.getElementsByName("comment-border");
  var elementsLength = elements.length;
  for (var i = 0; i < elementsLength; i++) {
    var tr = M_getParent(M_getParent(elements[i]));
    tr.style.display = "none";
    tr.name = "";
  }
  for (var i = 0; i < hide_elements.length; i++) {
    hide_elements[i].style.display = "none";
  }
  hookState.updateHooks();
}
/**
 * Flips between making inline comments visible and invisible.
 */
function M_toggleAllInlineComments() {
  var show_elements = document.getElementsByName("show-all-inline");
  if (!show_elements) {
    return;
  }
  if (show_elements[0].style.display == "none") {
    M_hideAllInlineComments();
  } else {
    M_showAllInlineComments();
  }
}
/**
 * Navigates to the diff with the requested versions on left/right
 */
function M_navigateDiff(issueid, filename) {
  var left = document.getElementById('left').value;
  var right = document.getElementById('right').value;
  if (left == '-1') {
    window.location.href = base_url + issueid + '/diff/' + right + '/' + filename;
  } else {
    window.location.href = base_url + issueid + '/diff2/' + left + ':' + right + '/' + filename;
  }
}
// File view keyboard navigation
/**
 * M_HookState class. Keeps track of the current 'hook' that we are on and
 * responds to n/p/N/P events.
 * @param {Window} win The window that the table is in.
 * @constructor
 */
function M_HookState(win) {
  /**
   * -2 == top of page; -1 == diff; or index into hooks array
   * @type Integer
   */
  this.hookPos = -2;
  /**
   * A cache of visible table rows with tr.name == "hook"
   * @type Array
   */
  this.visibleHookCache = [];
  /**
   * The indicator element that we move around
   * @type Element
   */
  this.indicator = document.getElementById("hook-sel");
  /**
   * The element the indicator points to
   * @type Element
   */
  this.indicated_element = null;
  /**
   * Caches whether we are in an IE browser
   * @type Boolean
   */
  this.isIE = M_isIE();
  /**
   * The window that the table with the hooks is in
   * @type Window
   */
  this.win = win;
}
/**
 * Find all the hook locations in a browser-portable fashion, and store them
 * in a cache.
 * @return Array of TR elements.
 */
M_HookState.prototype.computeHooks_ = function() {
  var allHooks = null;
  if (this.isIE) {
    // IE only recognizes the 'name' attribute on tags that are supposed to
    // have one, such as... not TR.
    var tmpHooks = document.getElementsByTagName("TR");
    var tmpHooksLength = tmpHooks.length;
    allHooks = [];
    for (var i = 0; i < tmpHooksLength; i++) {
      if (tmpHooks[i].name == "hook") {
        allHooks.push(tmpHooks[i]);
      }
    }
  } else {
    allHooks = document.getElementsByName("hook");
  }
  var visibleHooks = [];
  var allHooksLength = allHooks.length;
  for (var i = 0; i < allHooksLength; i++) {
    var hook = allHooks[i];
    if (hook.style.display == "") {
      visibleHooks.push(hook);
    }
  }
  this.visibleHookCache = visibleHooks;
  return visibleHooks;
};
/**
 * Recompute all the hook positions, update the hookPos, and update the
 * indicator's position if necessary, but do not scroll.
 */
M_HookState.prototype.updateHooks = function() {
  var curHook = null;
  if (this.indicated_element != null) {
    curHook = this.indicated_element;
  } else if (this.hookPos >= 0 && this.hookPos < this.visibleHookCache.length) {
    curHook = this.visibleHookCache[this.hookPos];
  }
  this.computeHooks_();
  var newHookPos = -1;
  if (curHook != null) {
    for (var i = 0; i < this.visibleHookCache.length; i++) {
      if (this.visibleHookCache[i] == curHook) {
        newHookPos = i;
        break;
      }
    }
  }
  if (newHookPos != -1) {
    this.hookPos = newHookPos;
  }
  this.gotoHook(0);
};
/**
 * Update the indicator's position to be at the top of the table row.
 * @param {Element} tr The tr whose top the indicator will be lined up with.
 */
M_HookState.prototype.updateIndicator_ = function(tr) {
  // Find out where the table's top is, and add one so that when we align
  // the position indicator, it takes off 1px from one tr and 1px from another.
  // This must be computed every time since the top of the table may move due
  // to window resizing.
  var tableTop = M_getPageOffsetTop(document.getElementById("table-top")) + 1;
  this.indicator.style.top = String(M_getPageOffsetTop(tr) -
                                    tableTop) + "px";
  var totWidth = 0;
  var numCells = tr.cells.length;
  for (var i = 0; i < numCells; i++) {
    totWidth += tr.cells[i].clientWidth;
  }
  this.indicator.style.left = "0px";
  this.indicator.style.width = totWidth + "px";
  this.indicator.style.display = "";
  this.indicated_element = tr;
};
/**
 * Update the indicator's position, and potentially scroll to the proper
 * location. Computes the new position based on current scroll position, and
 * whether the previously selected hook was visible.
 * @param {Integer} direction Scroll direction: -1 for up only, 1 for down only,
 *                            0 for no scrolling.
 */
M_HookState.prototype.gotoHook = function(direction) {
  var hooks = this.visibleHookCache;
  // Hide the current selection image
  this.indicator.style.display = "none";
  this.indicated_element = null;
  // Add a border to all td's in the selected row
  if (this.hookPos < -1) {
    if (direction != 0) {
      window.scrollTo(0, 0);
    }
    this.hookPos = -2;
  } else if (this.hookPos == -1) {
    var diffs = document.getElementsByName("diffs");
    if (diffs && diffs.length >= 1) {
      diffs = diffs[0];
    }
    if (diffs && direction != 0) {
      window.scrollTo(0, M_getPageOffsetTop(diffs) || 0);
    }
    this.updateIndicator_(document.getElementById("thecode").rows[0]);
  } else {
    if (this.hookPos < hooks.length) {
      var hook = hooks[this.hookPos];
      for (var i = 0; i < hook.cells.length; i++) {
        var td = hook.cells[i];
        if (td.id != null && td.id != "") {
          if (direction != 0) {
            M_scrollIntoView(this.win, td, direction);
          }
          break;
        }
      }
      // Found one!
      this.updateIndicator_(hook);
    } else {
      if (direction != 0) {
        window.scrollTo(0, document.body.offsetHeight);
      }
      this.hookPos = hooks.length;
      var thecode = document.getElementById("thecode");
      this.updateIndicator_(thecode.rows[thecode.rows.length - 1]);
    }
  }
};
/**
 * Update the indicator and hook position by moving to the next/prev line.
 * If the target line doesn't have a hook marker, the marker is added.
 * @param {Integer} direction Scroll direction: -1 for up, 1 for down.
 */
M_HookState.prototype.gotoLine = function(direction) {
  var thecode = document.getElementById("thecode").rows;
  // find current hook and store visible code lines
  var currHook = this.indicated_element;
  var hookIdx = -1;
  var codeRows = new Array();
  for (var i=0; i < thecode.length; i++) {
    if (thecode[i].id.substr(0, 4) == "pair") {
      codeRows.push(thecode[i]);
      if (currHook && thecode[i].id == currHook.id) {
        hookIdx = codeRows.length - 1;
      }
    }
  }
  if (direction > 0) {
    if (hookIdx == -1 && this.hookPos == -2) {  // not on a hook yet
      this.incrementHook_(false);
      this.gotoHook(0);
      return;
    } else if (hookIdx == -1 && this.indicated_element.id == "codeBottom") {
      // about to move off the borders
      return;
    } else if (hookIdx == codeRows.length - 1) {  // last row
      window.scrollTo(0, document.body.offsetHeight);
      this.hookPos = this.visibleHookCache.length;
      this.updateIndicator_(thecode[thecode.length - 1]);
      return;
    } else {
      hookIdx = Math.min(hookIdx + 1, codeRows.length - 1);
    }
  } else {
    if (hookIdx == -1 && this.hookPos < 0) {  // going beyond the top
      return;
    } else if (hookIdx == -1) { // we are at the bottom line
      hookIdx = codeRows.length - 1;
    } else if (hookIdx == 0) {  // we are at the top
      this.hookPos = -1;
      this.indicated_element = null;
      this.gotoHook(-1);
      return;
    } else {
      hookIdx = Math.max(hookIdx - 1, 0);
    }
  }
  var tr = codeRows[hookIdx];
  if (tr) {
    this.updateIndicator_(tr);
    M_scrollIntoView(this.win, tr, direction);
  }
}
/**
 * Updates hookPos relative to indicated line.
 * @param {Array} hooks Hook array.
 * @param {Integer} direction Wether to look for the next or prev element.
 */
M_HookState.prototype.updateHookPosByIndicator_ = function(hooks, direction) {
  if (this.indicated_element == null) {
    return;
  } else if (this.indicated_element.getAttribute("name") == "hook") {
    // hookPos is alread a hook
    return;
  }
  var indicatorLine = parseInt(this.indicated_element.id.split("-")[1]);
  for (var i=0; i < hooks.length; i++) {
    if (hooks[i].id.substr(0, 4) == "pair" &&
        parseInt(hooks[i].id.split("-")[1]) > indicatorLine) {
      if (direction > 0) {
        this.hookPos = i - 1;
      } else {
        this.hookPos = i;
      }
      return;
    }
  }
}
/**
 * Set this.hookPos to the next desired hook.
 * @param {Boolean} findComment Whether to look only for comment hooks
 */
M_HookState.prototype.incrementHook_ = function(findComment) {
  var hooks = this.visibleHookCache;
  if (this.indicated_line) {
    this.hookPos = this.findClosestHookPos_(hooks);
  }
  if (findComment) {
    this.hookPos = Math.max(0, this.hookPos + 1);
    while (this.hookPos < hooks.length &&
           hooks[this.hookPos].className != "inline-comments") {
      this.hookPos++;
    }
  } else {
    this.hookPos = Math.min(hooks.length, this.hookPos + 1);
  }
};
/**
 * Set this.hookPos to the previous desired hook.
 * @param {Boolean} findComment Whether to look only for comment hooks
 */
M_HookState.prototype.decrementHook_ = function(findComment) {
  var hooks = this.visibleHookCache;
  if (findComment) {
    this.hookPos = Math.min(hooks.length - 1, this.hookPos - 1);
    while (this.hookPos >= 0 &&
           hooks[this.hookPos].className != "inline-comments") {
      this.hookPos--;
    }
  } else {
    this.hookPos = Math.max(-2, this.hookPos - 1);
  }
};
/**
 * Find the first document element in sorted array elts whose vertical position
 * is greater than the given height from the top of the document. Optionally
 * look only for comment elements.
 *
 * @param {Integer} height The height in pixels from the top
 * @param {Array.} elts Document elements
 * @param {Boolean} findComment Whether to look only for comment elements
 * @return {Integer} The index of such an element, or elts.length otherwise
 */
function M_findElementAfter_(height, elts, findComment) {
  for (var i = 0; i < elts.length; ++i) {
    if (M_getPageOffsetTop(elts[i]) > height) {
      if (!findComment || elts[i].className == "inline-comments") {
        return i;
      }
    }
  }
  return elts.length;
}
/**
 * Find the last document element in sorted array elts whose vertical position
 * is less than the given height from the top of the document. Optionally
 * look only for comment elements.
 *
 * @param {Integer} height The height in pixels from the top
 * @param {Array.} elts Document elements
 * @param {Boolean} findComment Whether to look only for comment elements
 * @return {Integer} The index of such an element, or -1 otherwise
 */
function M_findElementBefore_(height, elts, findComment) {
  for (var i = elts.length - 1; i >= 0; --i) {
    if (M_getPageOffsetTop(elts[i]) < height) {
      if (!findComment || elts[i].className == "inline-comments") {
        return i;
      }
    }
  }
  return -1;
}
/**
 * Move to the next hook indicator and scroll.
 * @param opt_findComment {Boolean} Whether to look only for comment hooks
 */
M_HookState.prototype.gotoNextHook = function(opt_findComment) {
  // If the current hook is not on the page, select the first hook that is
  // either on the screen or below.
  var hooks = this.visibleHookCache;
  this.updateHookPosByIndicator_(hooks, 1);
  var diffs = document.getElementsByName("diffs");
  var thecode = document.getElementById("thecode");
  var findComment = Boolean(opt_findComment);
  if (diffs && diffs.length >= 1) {
    diffs = diffs[0];
  }
  if (this.hookPos >= 0 && this.hookPos < hooks.length &&
      M_isElementVisible(this.win, hooks[this.hookPos].cells[0])) {
    this.incrementHook_(findComment);
  } else if (this.hookPos == -2 &&
             (M_isElementVisible(this.win, diffs) ||
              M_getScrollTop(this.win) < M_getPageOffsetTop(diffs))) {
    this.incrementHook_(findComment)
  } else if (this.hookPos < hooks.length || (this.hookPos >= hooks.length &&
             !M_isElementVisible(
               this.win, thecode.rows[thecode.rows.length - 1].cells[0]))) {
    var scrollTop = M_getScrollTop(this.win);
    this.hookPos = M_findElementAfter_(scrollTop, hooks, findComment);
  }
  this.gotoHook(1);
};
/**
 * Move to the previous hook indicator and scroll.
 * @param opt_findComment {Boolean} Whether to look only for comment hooks
 */
M_HookState.prototype.gotoPrevHook = function(opt_findComment) {
  // If the current hook is not on the page, select the last hook that is
  // above the bottom of the screen window.
  var hooks = this.visibleHookCache;
  this.updateHookPosByIndicator_(hooks, -1);
  var diffs = document.getElementsByName("diffs");
  var findComment = Boolean(opt_findComment);
  if (diffs && diffs.length >= 1) {
    diffs = diffs[0];
  }
  if (this.hookPos == 0 && findComment) {
    this.hookPos = -2;
  } else if (this.hookPos >= 0 && this.hookPos < hooks.length &&
      M_isElementVisible(this.win, hooks[this.hookPos].cells[0])) {
    this.decrementHook_(findComment);
  } else if (this.hookPos > hooks.length) {
    this.hookPos = hooks.length;
  } else if (this.hookPos == -1 && M_isElementVisible(this.win, diffs)) {
    this.decrementHook_(findComment);
  } else if (this.hookPos == -2 && M_getScrollTop(this.win) == 0) {
  } else {
    var scrollBot = M_getScrollTop(this.win) + M_getWindowHeight(this.win);
    this.hookPos = M_findElementBefore_(scrollBot, hooks, findComment);
  }
  // The top of the diffs table is irrelevant if we want comment hooks.
  if (findComment && this.hookPos <= -1) {
    this.hookPos = -2;
  }
  this.gotoHook(-1);
};
/**
 * Finds the list of comments attached to the current hook, if any.
 *
 * @param self The calling object.
 * @return The list of comment DOM elements.
 */
function M_findCommentsForCurrentHook_(self) {
  var hooks = self.visibleHookCache;
  var hasHook = (self.hookPos >= 0 && self.hookPos < hooks.length &&
		 M_isElementVisible(self.win, hooks[self.hookPos].cells[0]));
  if (!hasHook)
    return [];
  // Go through this tr and collect divs.
  var comments = hooks[self.hookPos].getElementsByTagName("div");
  if (comments && comments.length == 0) {
    // Don't give up too early and look a bit forward
    var sibling = hooks[self.hookPos].nextSibling;
    while (sibling && sibling.tagName != "TR") {
      sibling = sibling.nextSibling;
    }
    comments = sibling.getElementsByTagName("div");
  }
  return comments;
}
/**
 * If the currently selected hook is a comment, either respond to it or edit
 * the draft if there is one already. Prefer the right side of the table.
 */
M_HookState.prototype.markAsDone = function() {
  var comments = M_findCommentsForCurrentHook_(this);
  var commentsLength = comments.length;
  if (!comments || !commentsLength)
    return;
  var last = null;
  // Try responding to the last comment. The general hope is that
  // these are returned in DOM order.
  for (var i = commentsLength - 1; i >= 0; i--) {
    if (comments[i].getAttribute("name") == "comment-border") {
      last = comments[i];
      break;
    }
  }
  if (!last)
    return;
  var links = last.getElementsByTagName("a");
  if (links) {
    for (var i = links.length - 1; i >= 0; i--) {
      if (links[i].getAttribute("name") == "comment-done" &&
          links[i].style.display != "none") {
        document.location.href = links[i].href;
	// Prevent done from being posted again.
	links[i].setAttribute("name", "");
        return;
      }
    }
  }
};
/**
 * If the currently selected hook is a comment, either respond to it or edit
 * the draft if there is one already. Prefer the right side of the table.
 */
M_HookState.prototype.respond = function() {
  if (this.indicated_element &&
      ! this.indicated_element.getAttribute("name") != "hook") {
    // Turn indicated element into a "real" hook so we can add comments.
    this.indicated_element.setAttribute("name", "hook");
  }
  this.updateHooks();
  var hooks = this.visibleHookCache;
  var hasHook = (this.hookPos >= 0 && this.hookPos < hooks.length &&
                 M_isElementVisible(this.win, hooks[this.hookPos].cells[0]));
  if (!hasHook)
    return;
  var comments = M_findCommentsForCurrentHook_(this);
  var commentsLength = comments.length;
  if (comments && commentsLength > 0) {
    var last = null;
    for (var i = commentsLength - 1; i >= 0; i--) {
      if (comments[i].getAttribute("name") == "comment-border") {
        last = comments[i];
        break;
      }
    }
    if (last) {
      var links = last.getElementsByTagName("a");
      if (links) {
        for (var i = links.length - 1; i >= 0; i--) {
          if (links[i].getAttribute("name") == "comment-reply" &&
              links[i].style.display != "none") {
            document.location.href = links[i].href;
            return;
          }
        }
      }
    }
  } else {
    // Create a comment at this line
    // TODO: Implement this in a sane fashion, e.g. opens up a comment
    // at the end of the diff chunk.
    var tr = hooks[this.hookPos];
    for (var i = tr.cells.length - 1; i >= 0; i--) {
      if (tr.cells[i].id.substr(0, 7) == "newcode") {
        M_createInlineComment(parseInt(tr.cells[i].id.substr(7)), 'b');
        return;
      } else if (tr.cells[i].id.substr(0, 7) == "oldcode") {
        M_createInlineComment(parseInt(tr.cells[i].id.substr(7)), 'a');
        return;
      }
    }
  }
};
// Intra-line diff handling
/**
 * IntraLineDiff class. Initializes structures to keep track of highlighting
 * state.
 * @constructor
 */
function M_IntraLineDiff() {
  /**
   * Whether we are showing intra-line changes or not
   * @type Boolean
   */
  this.intraLine = true;
  /**
   * "oldreplace" css rule
   * @type CSSStyleRule
   */
  this.oldReplace = null;
  /**
   * "oldlight" css rule
   * @type CSSStyleRule
   */
  this.oldLight = null;
  /**
   * "newreplace" css rule
   * @type CSSStyleRule
   */
  this.newReplace = null;
  /**
   * "newlight" css rule
   * @type CSSStyleRule
   */
  this.newLight = null;
  /**
   * backup of the "oldreplace" css rule's background color
   * @type DOMString
   */
  this.saveOldReplaceBkgClr = null;
  /**
   * backup of the "newreplace" css rule's background color
   * @type DOMString
   */
  this.saveNewReplaceBkgClr = null;
  /**
   * "oldreplace1" css rule's background color
   * @type DOMString
   */
  this.oldIntraBkgClr = null;
  /**
   * "newreplace1" css rule's background color
   * @type DOMString
   */
  this.newIntraBkgClr = null;
  this.findStyles_();
}
/**
 * Finds the styles in the document and keeps references to them in this class
 * instance.
 */
M_IntraLineDiff.prototype.findStyles_ = function() {
  var ss = document.styleSheets[0];
  var rules = [];
  if (ss.cssRules) {
    rules = ss.cssRules;
  } else if (ss.rules) {
    rules = ss.rules;
  }
  for (var i = 0; i < rules.length; i++) {
    var rule = rules[i];
    if (rule.selectorText == ".oldreplace1") {
      this.oldIntraBkgClr = rule.style.backgroundColor;
    } else if (rule.selectorText == ".newreplace1") {
      this.newIntraBkgClr = rule.style.backgroundColor;
    } else if (rule.selectorText == ".oldreplace") {
      this.oldReplace = rule;
      this.saveOldReplaceBkgClr = this.oldReplace.style.backgroundColor;
    } else if (rule.selectorText == ".newreplace") {
      this.newReplace = rule;
      this.saveNewReplaceBkgClr = this.newReplace.style.backgroundColor;
    } else if (rule.selectorText == ".oldlight") {
      this.oldLight = rule;
    } else if (rule.selectorText == ".newlight") {
      this.newLight = rule;
    }
  }
};
/**
 * Toggle the highlighting of the intra line diffs, alternatively turning
 * them on and off.
 */
M_IntraLineDiff.prototype.toggle = function() {
  if (this.intraLine) {
    this.oldReplace.style.backgroundColor = this.oldIntraBkgClr;
    this.oldLight.style.backgroundColor = this.oldIntraBkgClr;
    this.newReplace.style.backgroundColor = this.newIntraBkgClr;
    this.newLight.style.backgroundColor = this.newIntraBkgClr;
    this.intraLine = false;
  } else {
    this.oldReplace.style.backgroundColor = this.saveOldReplaceBkgClr;
    this.oldLight.style.backgroundColor = this.saveOldReplaceBkgClr;
    this.newReplace.style.backgroundColor = this.saveNewReplaceBkgClr;
    this.newLight.style.backgroundColor = this.saveNewReplaceBkgClr;
    this.intraLine = true;
  }
};
/**
 * A click handler common to just about every page, set in global.html.
 * @param {Event} evt The event object that triggered this handler.
 * @return false if the event was handled.
 */
function M_clickCommon(evt) {
  if (helpDisplayed) {
    var help = document.getElementById("help");
    help.style.display = "none";
    helpDisplayed = false;
    return false;
  }
  return true;
}
/**
 * Get a name for key combination of keydown event.
 *
 * See also http://unixpapa.com/js/key.html
 */
function M_getKeyName(evt) {
  var name = "";
  if (evt.ctrlKey)  { name += "Ctrl-" }
  if (evt.altKey)   { name += "Alt-" }
  if (evt.shiftKey) { name += "Shift-" }
  if (evt.metaKey) { name += "Meta-" }
  // Character keys have codes of corresponding ASCII symbols
  if (evt.keyCode >= 65 && evt.keyCode <= 90) {
    return name + String.fromCharCode(evt.keyCode);
  }
  // Numeric keys seems to have codes of corresponding ASCII symbols too
  if (evt.keyCode >= 48 && evt.keyCode <= 57) {
    return name + String.fromCharCode(evt.keyCode);
  }
  // Handling special keys
  switch (evt.keyCode) {
  case 27: return name + "Esc";
  case 13: return name + "Enter";
  case 188: return name + ",";  //  [,<]
  case 190: return name + ".";  //  [.>]
  case 191: return name + "/";  //  [/?]
  case 38: return name + "ArrowUp";
  case 40: return name + "ArrowDown";
  case 17: // Ctrl
  case 18: // Alt
  case 16: // Shift
  // case ??: Meta ?
           return name.substr(0, name.lenght-1);
  default:
    name += "<"+evt.keyCode+">";
  }
  return name;
}
/**
 * Common keydown handler for all pages.
 * @param {Event} evt The event object that triggered this callback
 * @param {function(string)} handler Handles the specific key name;
 *        returns false if the key was handled.
 * @param {function(Event, Node, int, string)} input_handler
 *        Handles the event in case that the event source is an input field.
 *        returns false if the key press was handled.
 * @return false if the event was handled
 */
function M_keyDownCommon(evt, handler, input_handler) {
  if (!evt) var evt = window.event; // for IE
  var target = M_getEventTarget(evt);
  var keyName = M_getKeyName(evt);
  if (target.nodeName == "TEXTAREA" || target.nodeName == "INPUT") {
    if (input_handler) {
      return input_handler(target, keyName);
    }
    return true;
  }
  if (keyName == 'Shift-/' /* '?' */ || keyName == 'Esc') {
    var help = document.getElementById("help");
    if (help) {
      // Only allow the help to be turned on with the ? key.
      if (helpDisplayed || keyName == 'Shift-/') {
        helpDisplayed = !helpDisplayed;
        help.style.display = helpDisplayed ? "" : "none";
        return false;
      }
    }
  }
  return handler(keyName);
}
/**
 * Helper event handler for the keydown event in a comment textarea.
 * @param {Event} evt The event object that triggered this callback
 * @param {Node} src The textarea document element
 * @param {String} key The string with combination name
 * @return false if the event was handled
 */
function M_commentTextKeyDown_(src, key) {
  if (src.nodeName == "TEXTAREA") {
    if (key == 'Ctrl-S' || key == 'Ctrl-Enter') {
      // Save the form corresponding to this text area.
      M_disableCarefulUnload();
      if (src.form.save.onclick) {
        return src.form.save.onclick();
      } else {
        src.form.submit();
        return false;
      }
    }
    if (key == 'Esc') {
      if (src.getAttribute('id') == draftMessage.id_textarea)
      {
        draftMessage.dialog_hide(true);
        src.blur();
        return false;
      } else {
        // textarea of inline comment
        return src.form.cancel.onclick();
      }
    }
  }
  return true;
}
/**
 * Helper to find an item by its elementId and jump to it.  If the item
 * cannot be found this will jump to the changelist instead.
 * @param {elementId} the id of an element an href
 */
function M_jumpToHrefOrChangelist(elementId) {
  var hrefElement = document.getElementById(elementId);
  if (hrefElement) {
    document.location.href = hrefElement.href;
  } else {
    M_upToChangelist();
  }
}
/**
 * Event handler for the keydown event in the file view.
 * @param {Event} evt The event object that triggered this callback
 * @return false if the event was handled
 */
function M_keyDown(evt) {
  return M_keyDownCommon(evt, function(key) {
    if (key == 'N') {
      // next diff
      if (hookState) hookState.gotoNextHook();
    } else if (key == 'P') {
      // previous diff
      if (hookState) hookState.gotoPrevHook();
    } else if (key == 'Shift-N') {
      // next comment
      if (hookState) hookState.gotoNextHook(true);
    } else if (key == 'Shift-P') {
      // previous comment
      if (hookState) hookState.gotoPrevHook(true);
    } else if (key == 'ArrowDown') {
      if (hookState) hookState.gotoLine(1);
    } else if (key == 'ArrowUp') {
      if (hookState) hookState.gotoLine(-1);
    } else if (key == 'J') {
      // next file
      M_jumpToHrefOrChangelist('nextFile')
    } else if (key == 'K') {
      // prev file
      M_jumpToHrefOrChangelist('prevFile')
    } else if (key == 'Shift-J') {
      // next file with comment
      M_jumpToHrefOrChangelist('nextFileWithComment')
    } else if (key == 'Shift-K') {
      // prev file with comment
      M_jumpToHrefOrChangelist('prevFileWithComment')
    } else if (key == 'M') {
      document.location.href = publish_link;
    } else if (key == 'Shift-M') {
      if (draftMessage) { draftMessage.dialog_show(); }
    } else if (key == 'U') {
      // up to CL
      M_upToChangelist();
    } else if (key == 'I') {
      // toggle intra line diff
      if (intraLineDiff) intraLineDiff.toggle();
    } else if (key == 'S') {
      // toggle show/hide inline comments
      M_toggleAllInlineComments();
    } else if (key == 'E') {
      M_expandAllInlineComments();
    } else if (key == 'C') {
      M_collapseAllInlineComments();
    } else if (key == 'Enter') {
      // respond to current comment
      if (hookState) hookState.respond();
    } else if (key == 'D') {
      // mark current comment as done
      if (hookState) hookState.markAsDone();
    } else {
      return true;
    }
    return false;
  }, M_commentTextKeyDown_);
}
/**
 * Event handler for the keydown event in the changelist (issue) view.
 * @param {Event} evt The event object that triggered this callback
 * @return false if the event was handled
 */
function M_changelistKeyDown(evt) {
  return M_keyDownCommon(evt, function(key) {
    if (key == 'O' || key == 'Enter') {
      if (dashboardState) {
	var child = dashboardState.curTR.cells[3].firstChild;
	while (child && child.nextSibling && child.nodeName != "A") {
	  child = child.nextSibling;
	}
	if (child && child.nodeName == "A") {
	  location.href = child.href;
	}
      }
    } else if (key == 'I') {
      if (dashboardState) {
	var child = dashboardState.curTR.cells[2].firstChild;
	while (child && child.nextSibling &&
	       (child.nodeName != "A" || child.style.display == "none")) {
	  child = child.nextSibling;
	}
	if (child && child.nodeName == "A") {
	  location.href = child.href;
	}
      }
    } else if (key == 'K') {
      if (dashboardState) dashboardState.gotoPrev();
    } else if (key == 'J') {
      if (dashboardState) dashboardState.gotoNext();
    } else if (key == 'M') {
      document.location.href = publish_link;
    } else if (key == 'U') {
      // back to dashboard
      document.location.href = base_url;
    } else if (key == 'Esc') {
      M_closePendingTrybots();
    } else {
      return true;
    }
    return false;
  });
}
/**
 * A mouse down handler for the change list page.  Dismissed the try bot
 * popup if visible.
 * @param {Event} evt The event object that triggered this handler.
 * @return false if the event was handled.
 */
function M_changelistMouseDown(evt) {
  var trybotPopup = document.getElementById('trybot-popup');
  if (trybotPopup && trybotPopup.style.display != 'none') {
    var target = M_getEventTarget(evt);
    while(target) {
      if (target == trybotPopup)
        return true;
      target = target.parentNode;
    }
    trybotPopup.style.display = 'none';
    return false;
  }
  return true;
}
/**
 * Goes from the file view back up to the changelist view.
 */
function M_upToChangelist() {
  var upCL = document.getElementById('upCL');
  if (upCL) {
    document.location.href = upCL.href;
  }
}
/**
 * Asynchronously request static analysis warnings as comments.
 * @param {String} cl The current changelist
 * @param {String} depot_path The id of the target element
 * @param {String} a The version number of the left side to be analyzed
 * @param {String} b The version number of the right side to be analyzed
 */
function M_getBugbotComments(cl, depot_path, a, b) {
  var httpreq = M_getXMLHttpRequest();
  if (!httpreq) {
    return;
  }
  // Konqueror jumps to a random location for some reason
  var scrollTop = M_getScrollTop(window);
  httpreq.onreadystatechange = function () {
    // Firefox 2.0, at least, runs this with readyState = 4 but all other
    // fields unset when the timeout aborts the request, against all
    // documentation.
    if (httpreq.readyState == 4) {
      if (httpreq.status == 200) {
        M_updateWarningStatus(httpreq.responseText);
      }
      if (M_isKHTML()) {
        window.scrollTo(0, scrollTop);
      }
    }
  }
  httpreq.open("GET", base_url + "warnings/" + cl + "/" + depot_path +
               "?a=" + a + "&b=" + b, true);
  httpreq.send(null);
}
/**
 * Updates a warning status td with the given HTML.
 * @param {String} result The new html to replace the existing content
 */
function M_updateWarningStatus(result) {
  var elem = document.getElementById("warnings");
  elem.innerHTML = result;
  if (hookState) hookState.updateHooks();
}
/* Ripped off from Caribou */
var M_CONFIRM_DISCARD_NEW_MSG = "Your draft comment has not been saved " +
                                "or sent.\n\nDiscard your comment?";
var M_useCarefulUnload = true;
/**
 * Return an alert if the specified textarea is visible and non-empty.
 */
function M_carefulUnload(text_area_id) {
  return function () {
    var text_area = document.getElementById(text_area_id);
    if (!text_area) return;
    var text_parent = M_getParent(text_area);
    if (M_useCarefulUnload && text_area.style.display != "none"
                           && text_parent.style.display != "none"
                           && goog.string.trim(text_area.value)) {
      return M_CONFIRM_DISCARD_NEW_MSG;
    }
  };
}
function M_disableCarefulUnload() {
  M_useCarefulUnload = false;
}
// History Table
/**
 * Toggles visibility of the snapshots that belong to the given parent.
 * @param {String} parent The parent's index
 * @param {Boolean} opt_show If present, whether to show or hide the group
 */
function M_toggleGroup(parent, opt_show) {
  var children = M_historyChildren[parent];
  if (children.length == 1) {  // No children.
    return;
  }
  var show = (typeof opt_show != "undefined") ? opt_show :
    (document.getElementById("history-" + children[1]).style.display != "");
  for (var i = 1; i < children.length; i++) {
    var child = document.getElementById("history-" + children[i]);
    child.style.display = show ? "" : "none";
  }
  var arrow = document.getElementById("triangle-" + parent);
  if (arrow) {
    arrow.className = "triangle-" + (show ? "open" : "closed");
  }
}
/**
 * Makes the given groups visible.
 * @param {Array.} parents The indexes of the parents of the groups
 *     to show.
 */
function M_expandGroups(parents) {
  for (var i = 0; i < parents.length; i++) {
    M_toggleGroup(parents[i], true);
  }
  document.getElementById("history-expander").style.display = "none";
  document.getElementById("history-collapser").style.display = "";
}
/**
 * Hides the given parents, except for groups that contain the
 * selected radio buttons.
 * @param {Array.} parents The indexes of the parents of the groups
 *     to hide.
 */
function M_collapseGroups(parents) {
  // Find the selected snapshots
  var parentsToLeaveOpen = {};
  var form = document.getElementById("history-form");
  var formLength = form.a.length;
  for (var i = 0; i < formLength; i++) {
    if (form.a[i].checked || form.b[i].checked) {
      var element = "history-" + form.a[i].value;
      var name = document.getElementById(element).getAttribute("name");
      if (name != "parent") {
        // The name of a child is "parent-%d" % parent_index.
        var parentIndex = Number(name.match(/parent-(\d+)/)[1]);
        parentsToLeaveOpen[parentIndex] = true;
      }
    }
  }
  // Collapse the parents we need to collapse.
  for (var i = 0; i < parents.length; i++) {
    if (!(parents[i] in parentsToLeaveOpen)) {
      M_toggleGroup(parents[i], false);
    }
  }
  document.getElementById("history-expander").style.display = "";
  document.getElementById("history-collapser").style.display = "none";
}
/**
 * Expands the reverted files section of the files list in the changelist view.
 *
 * @param {String} tableid The id of the table element that contains hidden TR's
 * @param {String} hide The id of the element to hide after this is completed.
 */
function M_showRevertedFiles(tableid, hide) {
  var table = document.getElementById(tableid);
  if (!table) return;
  var rowsLength = table.rows.length;
  for (var i = 0; i < rowsLength; i++) {
    var row = table.rows[i];
    if (row.getAttribute("name") == "afile") row.style.display = "";
  }
  if (dashboardState) dashboardState.initialize();
  var h = document.getElementById(hide);
  if (h) h.style.display = "none";
}
// Undo draft cancel
/**
 * An associative array mapping keys that identify inline comments to draft
 * text values.
 *   New inline comments have keys 'new-lineno-snapshot_id'
 *   Edit inline comments have keys 'edit-cid-lineno-side'
 *   Reply inline comments have keys 'reply-cid-lineno-side'
 * @type Object
 */
var M_savedInlineDrafts = new Object();
/**
 * Saves draft text from a form.
 * @param {String} draftKey The key identifying the saved draft text
 * @param {String} text The draft text to be saved
 */
function M_saveDraftText_(draftKey, text) {
  M_savedInlineDrafts[draftKey] = text;
}
/**
 * Clears saved draft text. Does nothing with an invalid key.
 * @param {String} draftKey The key identifying the saved draft text
 */
function M_clearDraftText_(draftKey) {
  delete M_savedInlineDrafts[draftKey];
}
/**
 * Restores saved draft text to a form. Does nothing with an invalid key.
 * @param {String} draftKey The key identifying the saved draft text
 * @param {Element} form The form that contains the comment to be restored
 * @param {Element} opt_selectAll Whether the restored text should be selected.
 *                                True by default.
 * @return {Boolean} true if we found a saved draft and false otherwise
 */
function M_restoreDraftText_(draftKey, form, opt_selectAll) {
  if (M_savedInlineDrafts[draftKey]) {
    form.text.value = M_savedInlineDrafts[draftKey];
    if (typeof opt_selectAll == 'undefined' || opt_selectAll) {
      form.text.select();
    }
    return true;
  }
  return false;
}
// Dashboard CL navigation
/**
 * M_DashboardState class. Keeps track of the current position of
 * the selector on the dashboard, and moves it on keydown.
 * @param {Window} win The window that the dashboard table is in.
 * @param {String} trName The name of TRs that we will move between.
 * @param {String} cookieName The cookie name to store the marker position into.
 * @constructor
 */
function M_DashboardState(win, trName, cookieName) {
  /**
   * The position of the marker, 0-indexed into the trCache array.
   * @ype Integer
   */
  this.trPos = 0;
  /**
   * The current TR object that the marker is pointing at.
   * @type Element
   */
  this.curTR = null;
  /**
   * Array of tr rows that we are moving between. Computed once (updateable).
   * @type Array
   */
  this.trCache = [];
  /**
   * The window that the table is in, used for positioning information.
   * @type Window
   */
  this.win = win;
  /**
   * The expected name of tr's that we are going to cache.
   * @type String
   */
  this.trName = trName;
  /**
   * The name of the cookie value where the marker position is stored.
   * @type String
   */
  this.cookieName = cookieName;
  this.initialize();
}
/**
 * Initializes the clCache array, and moves the marker into the first position.
 */
M_DashboardState.prototype.initialize = function() {
  var filter = function(arr, lambda) {
    var ret = [];
    var arrLength = arr.length;
    for (var i = 0; i < arrLength; i++) {
      if (lambda(arr[i])) {
	ret.push(arr[i]);
      }
    }
    return ret;
  };
  var cache;
  if (M_isIE()) {
    // IE does not recognize the 'name' attribute on TR tags
    cache = filter(document.getElementsByTagName("TR"),
		   function (elem) { return elem.name == this.trName; });
  } else {
    cache = document.getElementsByName(this.trName);
  }
  this.trCache = filter(cache, function (elem) {
    return elem.style.display != "none";
  });
  if (document.cookie && this.cookieName) {
    cookie_values = document.cookie.split(";");
    for (var i=0; i this.trCache.length-1) {
	  pos = 0;
	}
        this.trPos = pos;
      }
    }
  }
  this.goto_(0);
}
/**
 * Moves the cursor to the curCL position, and potentially scrolls the page to
 * bring the cursor into view.
 * @param {Integer} direction Positive for scrolling down, negative for
 *                            scrolling up, and 0 for no scrolling.
 */
M_DashboardState.prototype.goto_ = function(direction) {
  var oldTR = this.curTR;
  if (oldTR) {
    oldTR.cells[0].firstChild.style.visibility = "hidden";
  }
  this.curTR = this.trCache[this.trPos];
  this.curTR.cells[0].firstChild.style.visibility = "";
  if (this.cookieName) {
    document.cookie = this.cookieName+'='+this.trPos;
  }
  if (!M_isElementVisible(this.win, this.curTR)) {
    M_scrollIntoView(this.win, this.curTR, direction);
  }
}
/**
 * Moves the cursor up one.
 */
M_DashboardState.prototype.gotoPrev = function() {
  if (this.trPos > 0) this.trPos--;
  this.goto_(-1);
}
/**
 * Moves the cursor down one.
 */
M_DashboardState.prototype.gotoNext = function() {
  if (this.trPos < this.trCache.length - 1) this.trPos++;
  this.goto_(1);
}
/**
 * Event handler for dashboard hot keys. Dispatches cursor moves, as well as
 * opening CLs.
 */
function M_dashboardKeyDown(evt) {
  return M_keyDownCommon(evt, function(key) {
    if (key == 'K') {
      if (dashboardState) dashboardState.gotoPrev();
    } else if (key == 'J') {
      if (dashboardState) dashboardState.gotoNext();
    } else if (key == 'Shift-3' /* '#' */) {
      if (dashboardState) {
	var child = dashboardState.curTR.cells[1].firstChild;
	while (child && child.className != "issue-close") {
	  child = child.nextSibling;
	}
	if (child) {
	  child = child.firstChild;
	}
	while (child && child.nodeName != "A") {
	  child = child.nextSibling;
	}
	if (child) {
	  location.href = child.href;
	}
      }
    } else if (key == 'O' || key == 'Enter') {
      if (dashboardState) {
	var child = dashboardState.curTR.cells[2].firstChild;
	while (child && child.nodeName != "A") {
	  child = child.firstElementChild;
	}
	if (child) {
	  location.href = child.href;
	}
      }
    } else {
      return true;
    }
    return false;
  });
}
/**
 * Helper to fill a table cell element.
 * @param {Array} attrs An array of attributes to be applied
 * @param {String} text The content of the table cell
 * @return {Element}
 */
function M_fillTableCell_(attrs, text) {
  var td = document.createElement("td");
  for (var j=0; j';
    tr.innerHTML = html;
  }
  document.getElementById('skiploading-'+id_skip).style.visibility = 'visible';
  var context_select = document.getElementById('id_context');
  var context = null;
  if (context_select) {
    context = context_select.value;
  }
  var aborted = false;
  httpreq.onreadystatechange = function () {
    if (httpreq.readyState == 4 && !aborted) {
      if (httpreq.status == 200) {
        var response = eval('('+httpreq.responseText+')');
	var last_row = null;
        for (var i=0; i 0 ) {
          if ( where == 'b' ) {
            var new_before = id_before;
            var new_after = id_after-response.length/2;
          } else {
            var new_before = id_before+response.length/2;
            var new_after = id_after;
          }
          curr.innerHTML = new_count;
	  html = '';
	  if ( new_count > 3*context ) {
	    html += '';
	    html += 'Expand '+context+' before';
	    html += ' | ';
	  }
	  html += 'Expand all';
          if ( new_count > 3*context ) {
	    var val = parseInt(new_after);
            html += ' | ';
	    html += 'Expand '+context+' after';
	    html += '';
          }
          document.getElementById('skiplinks-'+(id_skip)).innerHTML = html;
	  var loading_node = document.getElementById('skiploading-'+id_skip);
	  loading_node.style.visibility = 'hidden';
        } else {
          tr.parentNode.removeChild(tr);
        }
	hookState.updateHooks();
        if (hookState.hookPos != -2 &&
	    M_isElementVisible(window, hookState.indicator)) {
	  // Restore indicator position on screen, but only if the indicator
	  // is visible. We don't know if we have to scroll up or down to
	  // make the indicator visible. Therefore the current hook is
	  // internally set to the previous hook and
	  // then gotoNextHook() does everything needed to end up with a
	  // clean hookState and the indicator visible on screen.
          hookState.hookPos = hookState.hookPos - 1;
	  hookState.gotoNextHook();
        }
      } else {
	msg = '';
	msg += 'An error occurred ['+httpreq.status+']. ';
	msg += 'Please report.';
	msg += '';
	tr.innerHTML = msg;
      }
    }
  }
  colwidth = document.getElementById('id_column_width').value;
  tabspaces = document.getElementById('id_tab_spaces').value;
  url = skipped_lines_url+id_before+'/'+id_after+'/'+where+'/'+colwidth+'/'+tabspaces;
  if (context) {
    url += '?context='+context;
  }
  httpreq.open('GET', url, true);
  httpreq.send('');
}
/**
 * Finds the element position.
 */
function M_getElementPosition(obj) {
  var curleft = curtop = 0;
  if (obj.offsetParent) {
    do {
      curleft += obj.offsetLeft;
      curtop += obj.offsetTop;
    } while (obj = obj.offsetParent);
  }
  return [curleft,curtop];
}
/**
 * Position the user info popup according to the mouse event coordinates
 */
function M_positionUserInfoPopup(obj, userPopupDiv) {
  pos = M_getElementPosition(obj);
  userPopupDiv.style.left = pos[0] + "px";
  userPopupDiv.style.top = pos[1] + 20 + "px";
}
/**
 * Brings up user info popup using ajax
 */
function M_showUserInfoPopup(obj) {
  var DIV_ID = "userPopupDiv";
  var userPopupDiv = document.getElementById(DIV_ID);
  var url = obj.getAttribute("href")
  var index = url.indexOf("/user/");
  var user_key = url.substring(index + 6);
  if (!userPopupDiv) {
    var userPopupDiv = document.createElement("div");
    userPopupDiv.className = "popup";
    userPopupDiv.id = DIV_ID;
    userPopupDiv.filter = 'alpha(opacity=85)';
    userPopupDiv.opacity = '0.85';
    userPopupDiv.innerHTML = "";
    userPopupDiv.onmouseout = function() {
      userPopupDiv.style.visibility = 'hidden';
    }
    document.body.appendChild(userPopupDiv);
  }
  M_positionUserInfoPopup(obj, userPopupDiv);
  var httpreq = M_getXMLHttpRequest();
  if (!httpreq) {
    return true;
  }
  var aborted = false;
  var httpreq_timeout = setTimeout(function() {
    aborted = true;
    httpreq.abort();
  }, 5000);
  httpreq.onreadystatechange = function () {
    if (httpreq.readyState == 4 && !aborted) {
      clearTimeout(httpreq_timeout);
      if (httpreq.status == 200) {
        userPopupDiv = document.getElementById(DIV_ID);
        userPopupDiv.innerHTML=httpreq.responseText;
        userPopupDiv.style.visibility = "visible";
      } else {
        //Better fail silently here because it's not
        //critical functionality
      }
    }
  }
  httpreq.open("GET", base_url + "user_popup/" + user_key, true);
  httpreq.send(null);
  obj.onmouseout = function() {
    aborted = true;
    userPopupDiv.style.visibility = 'hidden';
    obj.onmouseout = null;
  }
}
/**
 * TODO(jiayao,andi): docstring
 */
function M_showPopUp(obj, id) {
  var popup = document.getElementById(id);
  var pos = M_getElementPosition(obj);
  popup.style.left = pos[0]+'px';
  popup.style.top = pos[1]+20+'px';
  popup.style.visibility = 'visible';
  obj.onmouseout = function() {
    popup.style.visibility = 'hidden';
    obj.onmouseout = null;
  }
}
/**
 * Jump to a patch in the changelist.
 * @param {Element} select The select form element.
 * @param {Integer} issue The issue id.
 * @param {Integer} patchset The patchset id.
 * @param {Boolean} unified If True show unified diff else s-b-s view.
 * @param {String} opt_part The type of diff to jump to -- diff/diff2/patch
 */
function M_jumpToPatch(select, issue, patchset, unified, opt_part) {
  var part = opt_part;
  if (!part) {
    if (unified) {
      part = 'patch';
    } else {
      part = 'diff';
    }
  }
  var url = base_url+issue+'/'+part+'/'+patchset+'/'+select.value;
  var context = document.getElementById('id_context');
  var colwidth = document.getElementById('id_column_width');
  var tabspaces = document.getElementById('id_tab_spaces');
  if (context && colwidth && tabspaces) {
    url = url+'?context='+context.value+'&column_width='+colwidth.value+'&tab_spaces='+tabspaces.value;
  }
  document.location.href = url;
}
/**
 * Add or remove a star to/from the given issue.
 * @param {Integer} id The issue id.
 * @param {String} url The url fragment to append: "/star" or "/unstar".
 */
function M_setIssueStar_(id, url) {
  var httpreq = M_getXMLHttpRequest();
  if (!httpreq) {
    return true;
  }
  httpreq.onreadystatechange = function () {
    if (httpreq.readyState == 4) {
      if (httpreq.status == 200) {
	  var elem = document.getElementById("issue-star-" + id);
	  elem.innerHTML = httpreq.responseText;
      }
    }
  }
  httpreq.open("POST", base_url + id + url, true);
  httpreq.send("xsrf_token=" + xsrfToken);
}
/**
 * Add a star to the given issue.
 * @param {Integer} id The issue id.
 */
function M_addIssueStar(id) {
  return M_setIssueStar_(id, "/star");
}
/**
 * Remove the star from the given issue.
 * @param {Integer} id The issue id.
 */
function M_removeIssueStar(id) {
  return M_setIssueStar_(id, "/unstar");
}
/**
 * Close a given issue.
 * @param {Integer} id The issue id.
 */
function M_closeIssue(id) {
  var httpreq = M_getXMLHttpRequest();
  if (!httpreq) {
    return true;
  }
  httpreq.onreadystatechange = function () {
    if (httpreq.readyState == 4) {
      if (httpreq.status == 200) {
	  var elem = document.getElementById("issue-close-" + id);
	  elem.innerHTML = '';
	  var elem = document.getElementById("issue-title-" + id);
	  elem.innerHTML += ' (' + httpreq.responseText + ')';
      }
    }
  }
  httpreq.open("POST", base_url + id + "/close", true);
  httpreq.send("xsrf_token=" + xsrfToken);
}
/**
 * Generic callback when page is unloaded.
 */
function M_unloadPage() {
  if (draftMessage) { draftMessage.save(); }
}
/**
 * Draft message dialog class.
 * @param {Integer} issue_id ID of current issue.
 * @param {Boolean} headless If true, the dialog is not
                             initialized (default: false).
 */
var draftMessage = null;
function M_draftMessage(issue_id, headless) {
  this.issue_id = issue_id;
  this.id_dlg_container = 'reviewmsgdlg';
  this.id_textarea = 'reviewmsg';
  this.id_hidden = 'reviewmsgorig';
  this.id_status = 'reviewmsgstatus';
  this.is_modified = false;
  if (! headless) { this.initialize(); }
}
/**
 * Constructor.
 * Sets keydown callback and loads draft message if any.
 */
M_draftMessage.prototype.initialize = function() {
  this.load();
}
/**
 * Shows the dialog and focusses on textarea.
 */
M_draftMessage.prototype.dialog_show = function() {
  dlg = this.get_dialog_();
  dlg.style.display = "";
  this.set_status('');
  textarea = document.getElementById(this.id_textarea);
  textarea.focus();
}
/**
 * Hides the dialog and optionally saves the current content.
 * @param {Boolean} save If true, the content is saved.
 */
M_draftMessage.prototype.dialog_hide = function(save) {
  if (save) {
    this.save();
  }
  dlg = this.get_dialog_();
  dlg.style.display = "none";
}
/**
 * Discards draft message.
 */
M_draftMessage.prototype.dialog_discard = function() {
  this.discard(function(response) {
    draftMessage.set_status('OK');
    textarea = document.getElementById(draftMessage.id_textarea);
    textarea.value = '';
    });
  return false;
}
/**
 * Saves the content without closing the dialog.
 */
M_draftMessage.prototype.dialog_save = function() {
  this.set_status('');
  textarea = document.getElementById(this.id_textarea);
  this.save(function(response) {
    if (response.status != 200) {
      draftMessage.set_status('An error occurred.');
    } else {
      draftMessage.set_status('Message saved.');
    }
    textarea.focus();
    return false;
  });
  return false;
}
/**
 * Sets a status message in the dialog.
 * Additionally a timeout function is created to hide the message again.
 * @param {String} msg The message to display.
 */
M_draftMessage.prototype.set_status = function(msg) {
  var statusSpan = document.getElementById(this.id_status);
  if (statusSpan) {
    statusSpan.innerHTML = msg;
    if (msg) {
      this.status_timeout = setTimeout(function() {
        draftMessage.set_status('');
        }, 3000);
    }
  }
}
/**
 * Saves the content of the draft message.
 * @param {Function} cb A function called with the response object.
 */
M_draftMessage.prototype.save = function(cb) {
  textarea = document.getElementById(this.id_textarea);
  hidden = document.getElementById(this.id_hidden);
  if (textarea == null || textarea.value == hidden.value || textarea.value == "") {
    return;
  }
  text = textarea.value;
  var httpreq = M_getXMLHttpRequest();
  if (!httpreq) {
    return true;
  }
  httpreq.onreadystatechange = function () {
    if (httpreq.readyState == 4 && cb) {
      /* XXX set hidden before cb */
      hidden = document.getElementById(draftMessage.id_hidden);
      hidden.value = text;
      cb(httpreq);
    }
  }
  httpreq.open("POST", base_url + this.issue_id + "/draft_message", true);
  httpreq.send("reviewmsg="+encodeURIComponent(text));
}
/**
 * Loads the content of the draft message from the datastore.
 */
M_draftMessage.prototype.load = function() {
  elem = document.getElementById(this.id_textarea);
  elem.disabled = "disabled";
  this.set_status("Loading...");
  if (elem) {
    var httpreq = M_getXMLHttpRequest();
    if (!httpreq) {
      return true;
    }
    httpreq.onreadystatechange = function () {
      if (httpreq.readyState == 4) {
	if (httpreq.status != 200) {
	  draftMessage.set_status('An error occurred.');
	} else {
	  if (elem) {
	    elem.value = httpreq.responseText;
	    hidden = document.getElementById(draftMessage.id_hidden);
	    hidden.value = elem.value;
	  }
	}
	elem.removeAttribute("disabled");
	draftMessage.set_status('');
	elem.focus();
      }
    }
    httpreq.open("GET", base_url + this.issue_id + "/draft_message", true);
    httpreq.send("");
  }
}
/**
 * Discards the draft message.
 * @param {Function} cb A function called with response object.
 */
M_draftMessage.prototype.discard = function(cb) {
  var httpreq = M_getXMLHttpRequest();
  if (!httpreq) {
    return true;
  }
  httpreq.onreadystatechange = function () {
    if (httpreq.readyState == 4 && cb) {
      elem = document.getElementById(this.id_textarea);
      if (elem) {
	elem.value = "";
	hidden = document.getElementById(this.id_hidden);
	hidden.value = elem.value;
      }
      cb(httpreq);
    }
  }
  httpreq.open("DELETE", base_url + this.issue_id + "/draft_message", true);
  httpreq.send("");
}
/**
 * Helper function that returns the dialog's HTML container.
 */
M_draftMessage.prototype.get_dialog_ = function() {
  return document.getElementById(this.id_dlg_container);
} |