/**********************************************************
 * ABAPGIT JS function library
 **********************************************************/

/**********************************************************
  Global variables used from outside
 **********************************************************/
/* exported setInitialFocus */
/* exported setInitialFocusWithQuerySelector */
/* exported submitFormById */
/* exported errorStub */
/* exported confirmInitialized */
/* exported perfOut */
/* exported perfLog */
/* exported perfClear */
/* exported enableArrowListNavigation */
/* exported activateLinkHints */
/* exported setKeyBindings */
/* exported preparePatch */
/* exported registerStagePatch */
/* exported toggleRepoListDetail */
/* exported onTagTypeChange */
/* exported getIndocStyleSheet */
/* exported addMarginBottom */
/* exported enumerateJumpAllFiles */
/* exported createRepoCatalogEnumerator */
/* exported enumerateUiActions */
/* exported onDiffCollapse */
/* exported restoreScrollPosition */

/**********************************************************
 * Polyfills
 **********************************************************/

// Bind polyfill (for IE7), taken from https://developer.mozilla.org/
if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== "function") {
      throw new TypeError("Function.prototype.bind - subject is not callable");
    }

    var aArgs   = Array.prototype.slice.call(arguments, 1),
      fToBind = this,
      fNOP    = function() {},
      fBound  = function() {
        return fToBind.apply(
          this instanceof fNOP
            ? this
            : oThis,
          aArgs.concat(Array.prototype.slice.call(arguments))
        );
      };

    if (this.prototype) {
      fNOP.prototype = this.prototype;
    }
    fBound.prototype = new fNOP();

    return fBound;
  };
}

// String includes polyfill, taken from https://developer.mozilla.org
if (!String.prototype.includes) {
  String.prototype.includes = function(search, start) {
    "use strict";
    if (typeof start !== "number") {
      start = 0;
    }

    if (start + search.length > this.length) {
      return false;
    } else {
      return this.indexOf(search, start) !== -1;
    }
  };
}

// String startsWith polyfill, taken from https://developer.mozilla.org
if (!String.prototype.startsWith) {
  Object.defineProperty(String.prototype, "startsWith", {
    value: function(search, pos) {
      pos = !pos || pos < 0 ? 0 : +pos;
      return this.substring(pos, pos + search.length) === search;
    }
  });
}

// forEach polyfill, taken from https://developer.mozilla.org
// used for querySelectorAll results
if (window.NodeList && !NodeList.prototype.forEach) {
  NodeList.prototype.forEach = Array.prototype.forEach;
}

/**********************************************************
 * Common functions
 **********************************************************/

// Output text to the debug div
function debugOutput(text, dstID) {
  var stdout       = document.getElementById(dstID || "debug-output");
  var wrapped      = "<p>" + text + "</p>";
  stdout.innerHTML = stdout.innerHTML + wrapped;
}

// Use a pre-created form or create a hidden form
// and submit with sapevent
function submitSapeventForm(params, action, method) {

  function getSapeventPrefix() {
    if (document.querySelector('a[href*="file:///SAPEVENT:"]'))  {
      return "file:///"; //Prefix for chromium based browser control
    } else {
      return "";
    }
  }

  var stub_form_id = "form_" + action;
  var form = document.getElementById(stub_form_id);

  if (form === null) {
    form = document.createElement("form");
    form.setAttribute("method", method || "post");
    if (/sapevent/i.test(action)){
      form.setAttribute("action", action);
    } else {
      form.setAttribute("action", getSapeventPrefix() + "SAPEVENT:" + action);
    }
  }

  for(var key in params) {
    var hiddenField = document.createElement("input");
    hiddenField.setAttribute("type", "hidden");
    hiddenField.setAttribute("name", key);
    hiddenField.setAttribute("value", params[key]);
    form.appendChild(hiddenField);
  }

  if (form.id !== stub_form_id) {
    document.body.appendChild(form);
  }

  form.submit();
}

// Set focus to a control
function setInitialFocus(id) {
  document.getElementById(id).focus();
}

// Set focus to a element with query selector
function setInitialFocusWithQuerySelector(sSelector, bFocusParent) {
  var oSelected = document.querySelector(sSelector);

  if (oSelected) {
    if (bFocusParent) {
      oSelected.parentElement.focus();
    } else {
      oSelected.focus();
    }
  }

}

// Submit an existing form
function submitFormById(id) {
  document.getElementById(id).submit();
}

// JS error stub
function errorStub(event) {
  var element    = event.target || event.srcElement;
  var targetName = element.id || element.name || "???";
  alert("JS Error, please log an issue (@" + targetName + ")");
}

// confirm JS initilization
function confirmInitialized() {
  var errorBanner = document.getElementById("js-error-banner");
  if (errorBanner) {
    errorBanner.style.display = "none";
  }
  debugOutput("js: OK"); // Final final confirmation :)
}

/**********************************************************
 * Performance utils (for debugging)
 **********************************************************/

var gPerf = [];

function perfOut(prefix) {
  var totals = {};
  for (var i = gPerf.length - 1; i >= 0; i--) {
    if (!totals[gPerf[i].name]) totals[gPerf[i].name] = {count: 0, time: 0};
    totals[gPerf[i].name].time  += gPerf[i].time;
    totals[gPerf[i].name].count += 1;
  }

  var keys = Object.keys(totals);
  for (var j = keys.length - 1; j >= 0; j--) {
    console.log(prefix
      + " " + keys[j] + ": "
      + totals[keys[j]].time.toFixed(3) + "ms"
      + " (" + totals[keys[j]].count.toFixed() +")");
  }
}

function perfLog(name, startTime) {
  gPerf.push({name: name, time: window.performance.now() - startTime});
}

function perfClear() {
  gPerf = [];
}

/**********************************************************
 * TAG PAGE Logic
 **********************************************************/
function onTagTypeChange(oSelectObject){
  var sValue = oSelectObject.value;
  submitSapeventForm({ type: sValue }, "change_tag_type", "post");
}

/**********************************************************
 * Repo Overview Logic
 **********************************************************/
function findStyleSheetByName(name) {
  for (var s = 0; s < document.styleSheets.length; s++) {
    var styleSheet = document.styleSheets[s];
    var classes    = styleSheet.cssRules || styleSheet.rules;
    for (var i = 0; i < classes.length; i++) {
      if (classes[i].selectorText === name) return classes[i];
    }
  }
}

function getIndocStyleSheet() {
  for (var s = 0; s < document.styleSheets.length; s++) {
    if (!document.styleSheets[s].href) return document.styleSheets[s]; // One with empty href
  }
  // None found ? create one
  var style = document.createElement("style");
  document.head.appendChild(style);
  return style.sheet;
}

function RepoOverViewHelper() {
  this.setHooks();
  this.pageId = "RepoOverViewHelperState"; // constant is OK for this case
  this.isDetailsDisplayed = false;
  this.isOnlyFavoritesDisplayed = false;
  this.detailCssClass = findStyleSheetByName(".ro-detail");
  this.actionCssClass = findStyleSheetByName(".ro-action");
  var icon = document.getElementById("icon-filter-detail");
  this.toggleFilterIcon(icon, this.isDetailsDisplayed);
  this.registerRowSelection();
  this.registerKeyboardShortcuts();
}

RepoOverViewHelper.prototype.setHooks = function () {
  window.onload = this.onPageLoad.bind(this);
};

RepoOverViewHelper.prototype.onPageLoad = function () {
  var data = window.localStorage && JSON.parse(window.localStorage.getItem(this.pageId));
  if (data) {
    if (data.isDetailsDisplayed) {
      this.toggleItemsDetail(true);
    }
    if (data.selectedRepoKey) {
      this.selectRowByRepoKey(data.selectedRepoKey);
    } else {
      this.selectRowByIndex(0);
    }
  }
};

RepoOverViewHelper.prototype.registerKeyboardShortcuts = function() {
  var self = this;
  document.addEventListener("keypress", function(event) {
    if (document.activeElement.id === "filter") {
      return;
    }
    var keycode = event.keyCode;
    var rows = Array.prototype.slice.call(self.getVisibleRows());
    var selected = document.querySelector(".repo.selected");
    var indexOfSelected = rows.indexOf(selected);

    if (keycode == 13 && // "enter" to open
       document.activeElement.tagName.toLowerCase() != "input") { // prevent opening if command field has focus
      self.openSelectedRepo();
    } else if ((keycode == 52 || keycode == 100) && indexOfSelected > 0) {
      // "4" for previous
      self.selectRowByIndex(indexOfSelected - 1);
    } else if ((keycode == 54 || keycode == 102) && indexOfSelected < rows.length - 1) {
      // "6" for next
      self.selectRowByIndex(indexOfSelected + 1);
    }
  });
};

RepoOverViewHelper.prototype.openSelectedRepo = function () {
  this.selectedRepoKey = document.querySelector(".repo.selected").dataset.key;
  this.saveLocalStorage();
  document.querySelector(".repo.selected td.ro-go a").click();
};

RepoOverViewHelper.prototype.selectRowByIndex = function (index) {
  var rows = this.getVisibleRows();
  if (rows.length >= index) {
    var selectedRow = rows[index];
    if (selectedRow.classList.contains("selected")) {
      return;
    }

    this.deselectAllRows();
    rows[index].classList.add("selected");
    this.selectedRepoKey = selectedRow.dataset.key;
    this.updateActionLinks(selectedRow);
    this.saveLocalStorage();
  }
};

RepoOverViewHelper.prototype.selectRowByRepoKey = function (key) {
  var attributeQuery = "[data-key='" + key + "']";
  var row = document.querySelector(".repo" + attributeQuery);
  // navigation to already selected repo
  if (row.dataset.key === key && row.classList.contains("selected")) {
    return;
  }

  this.deselectAllRows();
  row.classList.add("selected");
  this.selectedRepoKey = key;
  this.updateActionLinks(row);
  this.saveLocalStorage();
};

RepoOverViewHelper.prototype.updateActionLinks = function (selectedRow) {
  // now we have a repo selected, determine which action buttons are relevant
  var selectedRepoKey = selectedRow.dataset.key;
  var selectedRepoIsOffline = selectedRow.dataset.offline === "X";

  var actionLinks = document.querySelectorAll("a.action_link");
  actionLinks.forEach(function (link) {
    // adjust repo key in urls
    link.href = link.href.replace(/\?key=(#|\d+)/, "?key=" + selectedRepoKey);

    // toggle button visibility
    if (link.classList.contains("action_offline_repo")) {
      if (selectedRepoIsOffline) {
        link.parentElement.classList.add("enabled");
      } else {
        link.parentElement.classList.remove("enabled");
      }
    }
    else if (link.classList.contains("action_online_repo")) {
      if (!selectedRepoIsOffline) {
        link.parentElement.classList.add("enabled");
      } else {
        link.parentElement.classList.remove("enabled");
      }
    }
    else {
      // if the action is for both repository types, it will only have the .action_link class
      // it still needs to be toggled as we want to hide everything if no repo is selected
      link.parentElement.classList.add("enabled");
    }
  });
};

RepoOverViewHelper.prototype.deselectAllRows = function () {
  document.querySelectorAll(".repo").forEach(function (x) {
    x.classList.remove("selected");
  });
};

RepoOverViewHelper.prototype.getVisibleRows = function () {
  return document.querySelectorAll(".repo:not(.nodisplay)");
};

RepoOverViewHelper.prototype.registerRowSelection = function () {
  var self = this;
  document.querySelectorAll(".repo td:not(.ro-go)").forEach(function (repoListRowCell) {
    repoListRowCell.addEventListener("click", function () {
      self.selectRowByRepoKey(this.parentElement.dataset.key);
    });
  });

  document.querySelectorAll(".repo td.ro-go").forEach(function (openRepoIcon) {
    openRepoIcon.addEventListener("click", function () {
      var selectedRow = this.parentElement;
      self.selectRowByRepoKey(selectedRow.dataset.key);
      self.openSelectedRepo();
    });
  });
};

RepoOverViewHelper.prototype.toggleRepoListDetail = function (forceDisplay) {
  if (this.detailCssClass) {
    this.toggleItemsDetail(forceDisplay);
    this.saveLocalStorage();
  }
};

RepoOverViewHelper.prototype.toggleItemsDetail = function (forceDisplay) {
  if (this.detailCssClass) {
    this.isDetailsDisplayed = forceDisplay || !this.isDetailsDisplayed;

    // change layout to wide if details are displayed
    if (this.isDetailsDisplayed) {
      document.body.classList.remove("centered");
      document.body.classList.add("full_width");
    } else {
      document.body.classList.add("centered");
      document.body.classList.remove("full_width");
    }

    this.detailCssClass.style.display = this.isDetailsDisplayed ? "" : "none";
    var icon = document.getElementById("icon-filter-detail");
    this.toggleFilterIcon(icon, this.isDetailsDisplayed);
  }
};

RepoOverViewHelper.prototype.toggleFilterIcon = function (icon, isEnabled) {
  if (isEnabled) {
    icon.classList.remove("grey");
    icon.classList.add("blue");
  } else {
    icon.classList.remove("blue");
    icon.classList.add("grey");
  }
};

RepoOverViewHelper.prototype.saveLocalStorage = function () {
  if (!window.localStorage) return;
  var data = {
    isDetailsDisplayed: this.isDetailsDisplayed,
    isOnlyFavoritesDisplayed: this.isOnlyFavoritesDisplayed,
    selectedRepoKey: this.selectedRepoKey,
  };
  window.localStorage.setItem(this.pageId, JSON.stringify(data));
};


/**********************************************************
 * STAGE PAGE Logic
 **********************************************************/

// Stage helper constructor
function StageHelper(params) {
  this.pageSeed        = params.seed;
  this.formAction      = params.formAction;
  this.patchAction     = params.patchAction;
  this.user            = params.user;
  this.selectedCount   = 0;
  this.filteredCount   = 0;
  this.lastFilterValue = "";

  // DOM nodes
  this.dom = {
    stageTab:          document.getElementById(params.ids.stageTab),
    commitAllBtn:      document.getElementById(params.ids.commitAllBtn),
    commitSelectedBtn: document.getElementById(params.ids.commitSelectedBtn),
    commitFilteredBtn: document.getElementById(params.ids.commitFilteredBtn),
    patchBtn:          document.getElementById(params.ids.patchBtn),
    objectSearch:      document.getElementById(params.ids.objectSearch),
    selectedCounter:   null,
    filteredCounter:   null,
  };
  this.findCounters();

  // Table columns (autodetection)
  this.colIndex      = this.detectColumns();
  this.filterTargets = ["name", "user", "transport"];

  // Constants
  this.HIGHLIGHT_STYLE = "highlight";
  this.STATUS = {
    add:    "A",
    remove: "R",
    ignore: "I",
    reset:  "?",
    isValid: function (status) { return "ARI?".indexOf(status) == -1 }
  };

  this.TEMPLATES = {
    cmdReset:  "<a>reset</a>",
    cmdLocal:  "<a>add</a>",
    cmdRemote: "<a>ignore</a><a>remove</a>"
  };

  this.setHooks();
  if (this.user) this.injectFilterMe();
  Hotkeys.addHotkeyToHelpSheet("^Enter", "Commit");
  this.dom.objectSearch.focus();
}

StageHelper.prototype.findCounters = function() {
  this.dom.selectedCounter = this.dom.commitSelectedBtn.querySelector("span.counter");
  this.dom.filteredCounter = this.dom.commitFilteredBtn.querySelector("span.counter");
};

StageHelper.prototype.injectFilterMe = function() {
  var tabFirstHead = this.dom.stageTab.tHead.rows[0];
  if (!tabFirstHead || tabFirstHead.className !== "local") {
    return; // for the case only "remove part" is displayed
  }
  var changedByHead = tabFirstHead.cells[this.colIndex.user];
  changedByHead.innerText = changedByHead.innerText + " (";
  var a = document.createElement("A");
  a.appendChild(document.createTextNode("me"));
  a.onclick = this.onFilterMe.bind(this);
  a.href = "#";
  changedByHead.appendChild(a);
  changedByHead.appendChild(document.createTextNode(")"));
};

StageHelper.prototype.onFilterMe = function() {
  this.dom.objectSearch.value = this.user;
  this.onFilter({ type: "keypress", which: 13, target: this.dom.objectSearch });
};

// Hook global click listener on table, load/unload actions
StageHelper.prototype.setHooks = function() {
  window.onkeypress                  = this.onCtrlEnter.bind(this);
  this.dom.stageTab.onclick          = this.onTableClick.bind(this);
  this.dom.commitSelectedBtn.onclick = this.submit.bind(this);
  this.dom.commitFilteredBtn.onclick = this.submitVisible.bind(this);
  this.dom.patchBtn.onclick          = this.submitPatch.bind(this);
  this.dom.objectSearch.oninput      = this.onFilter.bind(this);
  this.dom.objectSearch.onkeypress   = this.onFilter.bind(this);
  window.onbeforeunload              = this.onPageUnload.bind(this);
  window.onload                      = this.onPageLoad.bind(this);
};

// Detect column index
StageHelper.prototype.detectColumns = function() {
  var dataRow  = this.dom.stageTab.tBodies[0].rows[0];
  var colIndex = {};

  for (var i = dataRow.cells.length - 1; i >= 0; i--) {
    if (dataRow.cells[i].className) colIndex[dataRow.cells[i].className] = i;
  }

  return colIndex;
};

// Store table state on leaving the page
StageHelper.prototype.onPageUnload = function() {
  if (!window.sessionStorage) return;

  var data = this.collectData();
  window.sessionStorage.setItem(this.pageSeed, JSON.stringify(data));
};

// Re-store table state on entering the page
StageHelper.prototype.onPageLoad = function() {
  var data = window.sessionStorage && JSON.parse(window.sessionStorage.getItem(this.pageSeed));

  this.iterateStageTab(true, function (row) {
    var status = data && data[row.cells[this.colIndex["name"]].innerText];
    this.updateRow(row, status || this.STATUS.reset);
  });

  this.updateMenu();
  if (this.dom.objectSearch.value) {
    this.applyFilterValue(this.dom.objectSearch.value);
  }
};

// Table event handler, change status
StageHelper.prototype.onTableClick = function (event) {
  var target = event.target || event.srcElement;
  if (!target) return;

  var td;
  if (target.tagName === "A") {
    td = target.parentNode;
  } else if (target.tagName === "TD") {
    td = target;
    if (td.children.length === 1 && td.children[0].tagName === "A") {
      target = td.children[0];
    } else return;
  } else return;

  if (["TD","TH"].indexOf(td.tagName) == -1 || td.className != "cmd") return;

  var status    = this.STATUS[target.innerText]; // Convert anchor text to status
  var targetRow = td.parentNode;

  if (td.tagName === "TD") {
    this.updateRow(targetRow, status);
  } else { // TH
    this.iterateStageTab(true, function (row) {
      if (row.style.display !== "none"            // Not filtered out
        && row.className === targetRow.className  // Same context as header
      ) {
        this.updateRow(row, status);
      }
    });
  }

  this.updateMenu();
};

StageHelper.prototype.onCtrlEnter = function (e) {
  if (e.ctrlKey && (e.which === 10 || e.key === "Enter")){
    var clickMap = {
      "default":  this.dom.commitAllBtn,
      "selected": this.dom.commitSelectedBtn,
      "filtered": this.dom.commitFilteredBtn
    };
    clickMap[this.calculateActiveCommitCommand()].click();
  }
};

// Search object
StageHelper.prototype.onFilter = function (e) {
  if ( // Enter hit or clear, IE SUCKS !
    e.type === "input" && !e.target.value && this.lastFilterValue
    || e.type === "keypress" && (e.which === 13 || e.key === "Enter") && !e.ctrlKey ) {

    this.applyFilterValue(e.target.value);
    submitSapeventForm({ filterValue: e.target.value }, "stage_filter", "post");
  }
};

StageHelper.prototype.applyFilterValue = function(sFilterValue) {

  this.lastFilterValue = sFilterValue;
  this.filteredCount = this.iterateStageTab(true, this.applyFilterToRow, sFilterValue);
  this.updateMenu();

};

// Apply filter to a single stage line - hide or show
StageHelper.prototype.applyFilterToRow = function (row, filter) {
  // Collect data cells
  var targets = this.filterTargets.map(function(attr) {
    // Get the innermost tag with the text we want to filter
    // <td>text</td>: elem = td-tag
    // <td><span><i></i><a>text</a></span></td>: elem = a-tag
    var elem = row.cells[this.colIndex[attr]];
    var elemA = elem.getElementsByTagName("A")[0];
    if (elemA) elem = elemA;
    return {
      elem:      elem,
      plainText: elem.innerText.replace(/ /g, "\u00a0"), // without tags, with encoded spaces
      curHtml:   elem.innerHTML
    };
  }, this);

  var isVisible = false;

  // Apply filter to cells, mark filtered text
  for (var i = targets.length - 1; i >= 0; i--) {
    var target = targets[i];
    // Ignore case of filter
    var regFilter = new RegExp("("+filter+")", "gi");
    target.newHtml = (filter)
      ? target.plainText.replace(regFilter, "<mark>$1</mark>")
      : target.plainText;
    target.isChanged = target.newHtml !== target.curHtml;
    isVisible        = isVisible || !filter || target.newHtml !== target.plainText;
  }

  // Update DOM
  row.style.display = isVisible ? "" : "none";
  for (var j = targets.length - 1; j >= 0; j--) {
    if (targets[j].isChanged) targets[j].elem.innerHTML = targets[j].newHtml;
  }
  return isVisible ? 1 : 0;
};

// Get how status should affect object counter
StageHelper.prototype.getStatusImpact = function (status) {
  if (typeof status !== "string"
    || status.length !== 1
    || this.STATUS.isValid(status) ) {
    alert("Unknown status");
  } else {
    return (status !== this.STATUS.reset) ? 1 : 0;
  }
};

// Update table line
StageHelper.prototype.updateRow = function (row, newStatus) {
  var oldStatus = row.cells[this.colIndex["status"]].innerText;

  if (oldStatus !== newStatus) {
    this.updateRowStatus(row, newStatus);
    this.updateRowCommand(row, newStatus);
  } else if (!row.cells[this.colIndex["cmd"]].children.length) {
    this.updateRowCommand(row, newStatus); // For initial run
  }

  this.selectedCount += this.getStatusImpact(newStatus) - this.getStatusImpact(oldStatus);
};

// Update Status cell (render set of commands)
StageHelper.prototype.updateRowStatus = function (row, status) {
  row.cells[this.colIndex["status"]].innerText = status;
  if (status === this.STATUS.reset) {
    row.cells[this.colIndex["status"]].classList.remove(this.HIGHLIGHT_STYLE);
  } else {
    row.cells[this.colIndex["status"]].classList.add(this.HIGHLIGHT_STYLE);
  }
};

// Update Command cell (render set of commands)
StageHelper.prototype.updateRowCommand = function (row, status) {
  var cell = row.cells[this.colIndex["cmd"]];
  if (status === this.STATUS.reset) {
    cell.innerHTML = (row.className == "local")
      ? this.TEMPLATES.cmdLocal
      : this.TEMPLATES.cmdRemote;
  } else {
    cell.innerHTML = this.TEMPLATES.cmdReset;
  }
};

StageHelper.prototype.calculateActiveCommitCommand = function () {
  var active;
  if (this.selectedCount > 0) {
    active = "selected";
  } else if (this.lastFilterValue) {
    active = "filtered";
  } else {
    active = "default";
  }
  return active;
};

// Update menu items visibility
StageHelper.prototype.updateMenu = function () {
  var display = this.calculateActiveCommitCommand();
  if (display === "selected") this.dom.selectedCounter.innerText = this.selectedCount.toString();
  if (display === "filtered") this.dom.filteredCounter.innerText = this.filteredCount.toString();

  this.dom.commitAllBtn.style.display      = display === "default" ? "" : "none";
  this.dom.commitSelectedBtn.style.display = display === "selected" ? "" : "none";
  this.dom.commitFilteredBtn.style.display = display === "filtered" ? "" : "none";
};

// Submit stage state to the server
StageHelper.prototype.submit = function () {
  submitSapeventForm(this.collectData(), this.formAction);
};

StageHelper.prototype.submitVisible = function () {
  this.markVisiblesAsAdded();
  submitSapeventForm(this.collectData(), this.formAction);
};

StageHelper.prototype.submitPatch = function(){
  submitSapeventForm(this.collectData(), this.patchAction);
};

// Extract data from the table
StageHelper.prototype.collectData = function () {
  var data  = {};
  this.iterateStageTab(false, function (row) {
    data[row.cells[this.colIndex["name"]].innerText] = row.cells[this.colIndex["status"]].innerText;
  });
  return data;
};

StageHelper.prototype.markVisiblesAsAdded = function () {
  this.iterateStageTab(false, function (row) {
    // TODO refacotr, unify updateRow logic
    if (row.style.display === "" && row.className === "local") { // visible
      this.updateRow(row, this.STATUS.add);
    } else {
      this.updateRow(row, this.STATUS.reset);
    }
  });
};

// Table iteration helper
StageHelper.prototype.iterateStageTab = function (changeMode, cb /*, ...*/) {
  var restArgs = Array.prototype.slice.call(arguments, 2);
  var table    = this.dom.stageTab;
  var retTotal = 0;

  if (changeMode) {
    var scrollOffset = window.pageYOffset;
    this.dom.stageTab.style.display = "none";
  }

  for (var b = 0, bN = table.tBodies.length; b < bN; b++) {
    var tbody = table.tBodies[b];
    for (var r = 0, rN = tbody.rows.length; r < rN; r++) {
      var args = [tbody.rows[r]].concat(restArgs);
      var retVal = cb.apply(this, args); // callback
      if (typeof retVal === "number") retTotal += retVal;
    }
  }

  if (changeMode) {
    this.dom.stageTab.style.display = "";
    window.scrollTo(0, scrollOffset);
  }

  return retTotal;
};

/**********************************************************
 * Check list wrapper
 **********************************************************/

function CheckListWrapper(id, cbAction, cbActionOnlyMyChanges) {
  this.id         = document.getElementById(id);
  this.cbAction   = cbAction;
  this.cbActionOnlyMyChanges = cbActionOnlyMyChanges;
  this.id.onclick = this.onClick.bind(this);
}

CheckListWrapper.prototype.onClick = function(e) { // eslint-disable-line no-unused-vars
  // Get nodes
  var target = event.target || event.srcElement;
  if (!target) return;
  if (target.tagName !== "A") { target = target.parentNode } // icon clicked
  if (target.tagName !== "A") return;
  if (target.parentNode.tagName !== "LI") return;

  var nodeA    = target;
  var nodeLi   = target.parentNode;
  var nodeIcon = target.children[0];
  if (!nodeIcon.classList.contains("icon")) return;

  // Node updates
  var option   = nodeA.innerText;
  var oldState = nodeLi.getAttribute("data-check");
  if (oldState === null) return; // no data-check attribute - non-checkbox
  var newState = oldState === "X" ? false : true;

  if (newState) {
    nodeIcon.classList.remove("grey");
    nodeIcon.classList.add("blue");
    nodeLi.setAttribute("data-check", "X");
  } else {
    nodeIcon.classList.remove("blue");
    nodeIcon.classList.add("grey");
    nodeLi.setAttribute("data-check", "");
  }

  // Action callback, special handling for "Only My Changes"
  if(option === "Only my changes") {
    this.cbActionOnlyMyChanges(nodeLi.getAttribute("data-aux"), newState);

    // hide "Changed By" menu
  } else {
    this.cbAction(nodeLi.getAttribute("data-aux"), option, newState);
  }
};

/**********************************************************
 * Diff page logic
 **********************************************************/

// Diff helper constructor
function DiffHelper(params) {
  this.pageSeed    = params.seed;
  this.counter     = 0;
  this.stageAction = params.stageAction;

  // DOM nodes
  this.dom = {
    diffList:    document.getElementById(params.ids.diffList),
    stageButton: document.getElementById(params.ids.stageButton)
  };

  this.repoKey = this.dom.diffList.getAttribute("data-repo-key");
  if (!this.repoKey) return; // Unexpected

  this.dom.jump = document.getElementById(params.ids.jump);
  this.dom.jump.onclick = this.onJump.bind(this);

  // Checklist wrapper
  if (document.getElementById(params.ids.filterMenu)) {
    this.checkList = new CheckListWrapper(params.ids.filterMenu, this.onFilter.bind(this), this.onFilterOnlyMyChanges.bind(this));
    this.dom.filterButton = document.getElementById(params.ids.filterMenu).parentNode;
  }

  // Hijack stage command
  if (this.dom.stageButton) {
    this.dom.stageButton.href    = "#";
    this.dom.stageButton.onclick = this.onStage.bind(this);
  }
}

// Action on jump click
DiffHelper.prototype.onJump = function(e){
  var text = ((e.target && e.target.text) || e);
  if (!text) return;

  var elFile = document.querySelector("[data-file*='" + text + "']");
  if (!elFile) return;

  setTimeout(function(){
    elFile.scrollIntoView();
  }, 100);
};

// Action on filter click
DiffHelper.prototype.onFilter = function(attr, target, state) {
  this.applyFilter(attr, target, state);
  this.highlightButton(state);
};

DiffHelper.prototype.onFilterOnlyMyChanges = function(username, state) {
  this.applyOnlyMyChangesFilter(username, state);
  this.counter = 0;

  if(state) {
    this.dom.filterButton.classList.add("bgorange");
  } else {
    this.dom.filterButton.classList.remove("bgorange");
  }

  // apply logic on Changed By list items
  var changedByListItems = Array.prototype.slice.call(document.querySelectorAll("[data-aux*=changed-by]"));

  changedByListItems
    .map(function(item) {
      var nodeIcon = item.children[0].children[0];

      if (state === true) {
        if(item.innerText === username) { // current user
          item.style.display = "";
          item.setAttribute("data-check", "X");

          if(nodeIcon) {
            nodeIcon.classList.remove("grey");
            nodeIcon.classList.add("blue");
          }
        } else { // other users
          item.style.display = "none";
          item.setAttribute("data-check", "");
        }
      } else {
        item.style.display = "";
        item.setAttribute("data-check", "X");

        if(nodeIcon) {
          nodeIcon.classList.remove("grey");
          nodeIcon.classList.add("blue");
        }
      }
    });
};

DiffHelper.prototype.applyOnlyMyChangesFilter = function (username, state) {

  var jumpListItems = Array.prototype.slice.call(document.querySelectorAll("[id*=li_jump]"));

  this.iterateDiffList(function(div) {
    if (state === true) { // switching on "Only my changes" filter
      if (div.getAttribute("data-changed-by") === username) {
        div.style.display = state ? "" : "none";
      } else {
        div.style.display = state ? "none" : "";
      }
    } else { // disabling
      div.style.display = "";
    }

    // hide the file in the jump list
    var dataFile = div.getAttribute("data-file");
    jumpListItems
      .filter(function(item){ return dataFile.includes(item.text) })
      .map(function(item){ item.style.display = div.style.display });
  });
};

// Hide/show diff based on params
DiffHelper.prototype.applyFilter = function (attr, target, state) {

  var jumpListItems = Array.prototype.slice.call(document.querySelectorAll("[id*=li_jump]"));

  this.iterateDiffList(function(div) {
    if (div.getAttribute("data-"+attr) === target) {
      div.style.display = state ? "" : "none";

      // hide the file in the jump list
      var dataFile = div.getAttribute("data-file");
      jumpListItems
        .filter(function(item){ return dataFile.includes(item.text) })
        .map(function(item){ item.style.display = div.style.display });
    }
  });
};

// Action on stage -> save visible diffs as state for stage page
DiffHelper.prototype.onStage = function (e) { // eslint-disable-line no-unused-vars
  if (window.sessionStorage) {
    var data = this.buildStageCache();
    window.sessionStorage.setItem(this.pageSeed, JSON.stringify(data));
  }
  var getParams = {key: this.repoKey, seed: this.pageSeed};
  submitSapeventForm(getParams, this.stageAction, "get");
};

// Collect visible diffs
DiffHelper.prototype.buildStageCache = function () {
  var list = {};
  this.iterateDiffList(function(div) {
    var filename = div.getAttribute("data-file");
    if (!div.style.display && filename) { // No display override - visible !!
      list[filename] = "A"; // Add
    }
  });
  return list;
};

// Table iterator
DiffHelper.prototype.iterateDiffList = function (cb /*, ...*/) {
  var restArgs = Array.prototype.slice.call(arguments, 1);
  var diffList = this.dom.diffList;

  for (var i = 0, iN = diffList.children.length; i < iN; i++) {
    var div = diffList.children[i];
    if (div.className !== "diff") continue;
    var args = [div].concat(restArgs);
    cb.apply(this, args); // callback
  }
};

// Highlight Filter button if filter is activate
DiffHelper.prototype.highlightButton = function(state) {
  this.counter += state ? -1 : 1;
  if (this.counter > 0) {
    this.dom.filterButton.classList.add("bgorange");
  } else {
    this.dom.filterButton.classList.remove("bgorange");
  }
};

//Collapse/Expand diffs
function onDiffCollapse(event) {
  var source = event.target || event.srcElement;
  var nextDiffContent = source.parentElement.nextElementSibling;
  var hide;

  if(source.classList.contains("icon-chevron-down")){
    source.classList.remove("icon-chevron-down");
    source.classList.add("icon-chevron-right");
    hide = true;
  } else {
    source.classList.remove("icon-chevron-right");
    source.classList.add("icon-chevron-down");
    hide = false;
  }

  hide ? nextDiffContent.classList.add("nodisplay") : nextDiffContent.classList.remove("nodisplay");
}

// Add Bottom margin, so that we can scroll to the top of the last file
function addMarginBottom(){
  document.getElementsByTagName("body")[0].style.marginBottom = screen.height + "px";
}


/**********************************************************
 * Diff page logic of column selection
 **********************************************************/

function DiffColumnSelection() {
  this.selectedColumnIdx = -1;
  this.lineNumColumnIdx = -1;
  //https://stackoverflow.com/questions/2749244/javascript-setinterval-and-this-solution
  document.addEventListener("mousedown", this.mousedownEventListener.bind(this));
  document.addEventListener("copy", this.copyEventListener.bind(this));
}

DiffColumnSelection.prototype.mousedownEventListener = function(e) {
  // Select text in a column of an HTML table and copy to clipboard (in DIFF view)
  // (https://stackoverflow.com/questions/6619805/select-text-in-a-column-of-an-html-table)
  // Process mousedown event for all TD elements -> apply CSS class at TABLE level.
  // (https://stackoverflow.com/questions/40956717/how-to-addeventlistener-to-multiple-elements-in-a-single-line)
  var unifiedLineNumColumnIdx = 0;
  var unifiedCodeColumnIdx = 3;
  var splitLineNumLeftColumnIdx = 0;
  var splitCodeLeftColumnIdx = 2;
  var splitLineNumRightColumnIdx = 3;
  var splitCodeRightColumnIdx = 5;

  if (e.button !== 0) return; // function is only valid for left button, not right button

  var td = e.target;
  while (td != undefined && td.tagName != "TD" && td.tagName != "TBODY") td = td.parentElement;
  if (td == undefined) return;
  var table = td.parentElement.parentElement;

  var patchColumnCount = 0;
  if (td.parentElement.cells[0].classList.contains("patch")) {
    patchColumnCount = 1;
  }

  if (td.classList.contains("diff_left")) {
    table.classList.remove("diff_select_right");
    table.classList.add("diff_select_left");
    if ( window.getSelection() && this.selectedColumnIdx != splitCodeLeftColumnIdx + patchColumnCount ) {
      // De-select to avoid effect of dragging selection in case the right column was first selected
      if (document.body.createTextRange) { // All IE but Edge
        // document.getSelection().removeAllRanges() may trigger error
        // so use this code which is equivalent but does not fail
        // (https://stackoverflow.com/questions/22914075/javascript-error-800a025e-using-range-selector)
        range = document.body.createTextRange();
        range.collapse();
        range.select();
      } else {
        document.getSelection().removeAllRanges();
      }}
    this.selectedColumnIdx = splitCodeLeftColumnIdx + patchColumnCount;
    this.lineNumColumnIdx = splitLineNumLeftColumnIdx + patchColumnCount;

  } else if (td.classList.contains("diff_right")) {
    table.classList.remove("diff_select_left");
    table.classList.add("diff_select_right");
    if ( window.getSelection() && this.selectedColumnIdx != splitCodeRightColumnIdx + patchColumnCount ) {
      if (document.body.createTextRange) { // All IE but Edge
        // document.getSelection().removeAllRanges() may trigger error
        // so use this code which is equivalent but does not fail
        // (https://stackoverflow.com/questions/22914075/javascript-error-800a025e-using-range-selector)
        var range = document.body.createTextRange();
        range.collapse();
        range.select();
      } else {
        document.getSelection().removeAllRanges();
      }}
    this.selectedColumnIdx = splitCodeRightColumnIdx + patchColumnCount;
    this.lineNumColumnIdx = splitLineNumRightColumnIdx + patchColumnCount;

  } else if (td.classList.contains("diff_unified")) {
    this.selectedColumnIdx = unifiedCodeColumnIdx;
    this.lineNumColumnIdx = unifiedLineNumColumnIdx;

  } else {
    this.selectedColumnIdx = -1;
    this.lineNumColumnIdx = -1;
  }
};

DiffColumnSelection.prototype.copyEventListener = function(e) {
  // Select text in a column of an HTML table and copy to clipboard (in DIFF view)
  // (https://stackoverflow.com/questions/6619805/select-text-in-a-column-of-an-html-table)
  var td = e.target;
  while (td != undefined && td.tagName != "TD" && td.tagName != "TBODY") td = td.parentElement;
  if(td != undefined){
    // Use window.clipboardData instead of e.clipboardData
    // (https://stackoverflow.com/questions/23470958/ie-10-copy-paste-issue)
    var clipboardData = ( e.clipboardData == undefined ? window.clipboardData : e.clipboardData );
    var text = this.getSelectedText();
    clipboardData.setData("text", text);
    e.preventDefault();
  }
};

DiffColumnSelection.prototype.getSelectedText = function() {
  // Select text in a column of an HTML table and copy to clipboard (in DIFF view)
  // (https://stackoverflow.com/questions/6619805/select-text-in-a-column-of-an-html-table)
  var sel = window.getSelection(),
    range = sel.getRangeAt(0),
    doc = range.cloneContents(),
    nodes = doc.querySelectorAll("tr"),
    text = "";
  if (nodes.length === 0) {
    text = doc.textContent;
  } else {
    var newline = "",
      realThis = this;
    [].forEach.call(nodes, function(tr, i) {
      var cellIdx = ( i==0 ? 0 : realThis.selectedColumnIdx );
      if (tr.cells.length > cellIdx) {
        var tdSelected = tr.cells[cellIdx];
        var tdLineNum = tr.cells[realThis.lineNumColumnIdx];
        // copy is interesting for remote code, don't copy lines which exist only locally
        if (i==0 || tdLineNum.getAttribute("line-num")!="") {
          text += newline + tdSelected.textContent;
          // special processing for TD tag which sometimes contains newline
          // (expl: /src/ui/zabapgit_js_common.w3mi.data.js) so don't add newline again in that case.
          var lastChar = tdSelected.textContent[ tdSelected.textContent.length - 1 ];
          if ( lastChar == "\n" ) newline = "";
          else newline = "\n";
        }}});}
  return text;
};

/**********************************************************
 * Other functions
 **********************************************************/

// News announcement
function toggleDisplay(divId) {
  var div = document.getElementById(divId);
  if (div) div.style.display = (div.style.display) ? "" : "none";
}

function KeyNavigation() { }

KeyNavigation.prototype.onkeydown = function(event) {
  if (event.defaultPrevented) return;

  // navigate with arrows through list items and support pressing links with enter and space
  var isHandled = false;
  if (event.key === "Enter" || event.key === "") {
    isHandled = this.onEnterOrSpace();
  } else if (/Down$/.test(event.key)) {
    isHandled = this.onArrowDown();
  } else if (/Up$/.test(event.key)) {
    isHandled = this.onArrowUp();
  } else if (event.key === "Backspace") {
    isHandled = this.onBackspace();
  }

  if (isHandled) event.preventDefault();
};

KeyNavigation.prototype.onEnterOrSpace = function () {
  if (document.activeElement.nodeName !== "A") return;
  var anchor = document.activeElement;

  if (anchor.href.replace(/#$/, "") === document.location.href.replace(/#$/, "")
    && !anchor.onclick
    && anchor.parentElement
    && anchor.parentElement.nodeName === "LI" ) {
    anchor.parentElement.classList.toggle("force-nav-hover");
  } else {
    anchor.click();
  }
  return true;
};

KeyNavigation.prototype.focusListItem = function (li) {
  var anchor = li.firstElementChild;
  if (!anchor || anchor.nodeName !== "A") return false;
  anchor.focus();
  return true;
};

KeyNavigation.prototype.closeDropdown = function (dropdownLi) {
  dropdownLi.classList.remove("force-nav-hover");
  if (dropdownLi.firstElementChild.nodeName === "A") dropdownLi.firstElementChild.focus();
  return true;
};

KeyNavigation.prototype.onBackspace = function () {
  var activeElement = document.activeElement;

  // Detect opened subsequent dropdown
  if (activeElement.nodeName === "A"
    && activeElement.parentElement
    && activeElement.parentElement.nodeName === "LI"
    && activeElement.parentElement.classList.contains("force-nav-hover")) {
    return this.closeDropdown(activeElement.parentElement);
  }

  // Detect opened parent dropdown
  if (activeElement.nodeName === "A"
    && activeElement.parentElement
    && activeElement.parentElement.nodeName === "LI"
    && activeElement.parentElement.parentElement
    && activeElement.parentElement.parentElement.nodeName === "UL"
    && activeElement.parentElement.parentElement.parentElement
    && activeElement.parentElement.parentElement.parentElement.nodeName === "LI"
    && activeElement.parentElement.parentElement.parentElement.classList.contains("force-nav-hover")) {
    return this.closeDropdown(activeElement.parentElement.parentElement.parentElement);
  }
};

KeyNavigation.prototype.onArrowDown = function () {
  var activeElement = document.activeElement;

  // Start of dropdown list: LI > selected A :: UL > LI > A
  if (activeElement.nodeName === "A"
    && activeElement.parentElement
    && activeElement.parentElement.nodeName === "LI"
    && activeElement.parentElement.classList.contains("force-nav-hover") // opened dropdown
    && activeElement.nextElementSibling
    && activeElement.nextElementSibling.nodeName === "UL"
    && activeElement.nextElementSibling.firstElementChild
    && activeElement.nextElementSibling.firstElementChild.nodeName === "LI") {
    return this.focusListItem(activeElement.nextElementSibling.firstElementChild);
  }

  // Next item of dropdown list: ( LI > selected A ) :: LI > A
  if (activeElement.nodeName === "A"
    && activeElement.parentElement
    && activeElement.parentElement.nodeName === "LI"
    && activeElement.parentElement.nextElementSibling
    && activeElement.parentElement.nextElementSibling.nodeName === "LI") {
    return this.focusListItem(activeElement.parentElement.nextElementSibling);
  }
};

KeyNavigation.prototype.onArrowUp = function () {
  var activeElement = document.activeElement;

  // Prev item of dropdown list: ( LI > selected A ) <:: LI > A
  if (activeElement.nodeName === "A"
    && activeElement.parentElement
    && activeElement.parentElement.nodeName === "LI"
    && activeElement.parentElement.previousElementSibling
    && activeElement.parentElement.previousElementSibling.nodeName === "LI") {
    return this.focusListItem(activeElement.parentElement.previousElementSibling);
  }
};

KeyNavigation.prototype.getHandler = function () {
  return this.onkeydown.bind(this);
};

// this functions enables the navigation with arrows through list items (li)
// e.g. in dropdown menus
function enableArrowListNavigation() {
  document.addEventListener("keydown", new KeyNavigation().getHandler());
}

/* LINK HINTS - Vimium like link hints */

function LinkHints(linkHintHotKey){
  this.linkHintHotKey    = linkHintHotKey;
  this.areHintsDisplayed = false;
  this.pendingPath       = ""; // already typed code prefix
  this.hintsMap          = this.deployHintContainers();
  this.activatedDropdown = null;
}

LinkHints.prototype.getHintStartValue = function(targetsCount){
  // e.g. if we have 89 tooltips we start from 10
  //      if we have 90 tooltips we start from 100
  //      if we have 900 tooltips we start from 1000
  var
    baseLength = Math.pow(10, targetsCount.toString().length - 1),
    maxHintStringLength = (targetsCount + baseLength).toString().length;
  return Math.pow(10, maxHintStringLength - 1);
};

LinkHints.prototype.deployHintContainers = function() {

  var hintTargets = document.querySelectorAll("a, input, textarea, i");
  var codeCounter = this.getHintStartValue(hintTargets.length);
  var hintsMap    = { first: codeCounter };

  // <span class="link-hint" data-code="123">
  //   <span class="pending">12</span><span>3</span>
  // </span>
  for (var i = 0, N = hintTargets.length; i < N; i++) {
    // skip hidden fields
    if (hintTargets[i].type === "HIDDEN") {
      continue;
    }

    var hint = {};
    hint.container     = document.createElement("span");
    hint.pendingSpan   = document.createElement("span");
    hint.remainingSpan = document.createElement("span");
    hint.parent        = hintTargets[i];
    hint.code          = codeCounter.toString();

    hint.container.appendChild(hint.pendingSpan);
    hint.container.appendChild(hint.remainingSpan);

    hint.pendingSpan.classList.add("pending");
    hint.container.classList.add("link-hint");
    if (hint.parent.nodeName === "INPUT" || hint.parent.nodeName === "TEXTAREA"){
      hint.container.classList.add("link-hint-input");
    } else if (hint.parent.nodeName === "A") {
      hint.container.classList.add("link-hint-a");
    } else if (hint.parent.nodeName === "I" && hint.parent.classList.contains("cursor-pointer")) {
      hint.container.classList.add("link-hint-i");
    } else {
      continue;
    }

    hint.container.classList.add("nodisplay");            // hide by default
    hint.container.dataset.code = codeCounter.toString(); // not really needed, more for debug

    if (hintTargets[i].nodeName === "INPUT" || hintTargets[i].nodeName === "TEXTAREA") {
      // does not work if inside the input node
      if (hintTargets[i].type === "checkbox" || hintTargets[i].type === "radio") {
        if (hintTargets[i].nextElementSibling && hintTargets[i].nextElementSibling.nodeName === "LABEL" ) {
          // insert at end of label
          hintTargets[i].nextElementSibling.appendChild(hint.container);
        } else {
          // inserting right after
          hintTargets[i].insertAdjacentElement("afterend", hint.container);
        }
      } else {
        // inserting right after
        hintTargets[i].insertAdjacentElement("afterend", hint.container);
      }
    } else {
      hintTargets[i].appendChild(hint.container);
    }
    hintsMap[codeCounter++] = hint;
  }

  hintsMap.last = codeCounter - 1;
  return hintsMap;
};

LinkHints.prototype.getHandler = function() {
  return this.handleKey.bind(this);
};

LinkHints.prototype.handleKey = function(event){

  if (event.defaultPrevented) {
    return;
  }

  if (event.key === this.linkHintHotKey && Hotkeys.isHotkeyCallPossible()) {

    // on user hide hints, close an opened dropdown too
    if (this.areHintsDisplayed && this.activatedDropdown) this.closeActivatedDropdown();

    this.pendingPath = "";
    this.displayHints(!this.areHintsDisplayed);

  } else if (this.areHintsDisplayed) {

    // the user tries to reach a hint
    this.pendingPath += event.key;
    var hint = this.hintsMap[this.pendingPath];

    if (hint) { // we are there, we have a fully specified tooltip. Let's activate it
      this.displayHints(false);
      event.preventDefault();
      this.hintActivate(hint);
    } else {
      // we are not there yet, but let's filter the link so that only
      // the partially matched are shown
      var visibleHints = this.filterHints();
      if (!visibleHints) {
        this.displayHints(false);
        if (this.activatedDropdown) this.closeActivatedDropdown();
      }
    }
  }
};

LinkHints.prototype.closeActivatedDropdown = function() {
  if (!this.activatedDropdown) return;
  this.activatedDropdown.classList.remove("force-nav-hover");
  this.activatedDropdown = null;
};

LinkHints.prototype.displayHints = function(isActivate) {
  this.areHintsDisplayed = isActivate;
  for (var i = this.hintsMap.first; i <= this.hintsMap.last; i++) {
    var hint = this.hintsMap[i];
    if (isActivate) {
      hint.container.classList.remove("nodisplay");
      hint.pendingSpan.innerText   = "";
      hint.remainingSpan.innerText = hint.code;
    } else {
      hint.container.classList.add("nodisplay");
    }
  }
};

LinkHints.prototype.hintActivate = function (hint) {
  if (hint.parent.nodeName === "A"
    // hint.parent.href doesn't have a # at the end while accessing dropdowns the first time.
    // Seems like a idiosyncrasy of SAPGUI's IE. So let's ignore the last character.
    && ( hint.parent.href.substr(0, hint.parent.href.length - 1) === document.location.href ) // href is #
    && !hint.parent.onclick                         // no handler
    && hint.parent.parentElement && hint.parent.parentElement.nodeName === "LI") {
    // probably it is a dropdown ...
    this.activatedDropdown = hint.parent.parentElement;
    this.activatedDropdown.classList.toggle("force-nav-hover");
    hint.parent.focus();
  } else if (hint.parent.type === "checkbox" || hint.parent.type === "radio") {
    this.toggleCheckbox(hint);
  } else if (hint.parent.type === "submit") {
    hint.parent.click();
  } else if (hint.parent.nodeName === "INPUT" || hint.parent.nodeName === "TEXTAREA") {
    hint.parent.focus();
  } else {
    hint.parent.click();
    if (this.activatedDropdown) this.closeActivatedDropdown();
  }
};

LinkHints.prototype.toggleCheckbox = function (hint) {
  // ensures that onclick handler is executed
  // https://stackoverflow.com/questions/41981509/trigger-an-event-when-a-checkbox-is-changed-programmatically-via-javascript
  var event = document.createEvent("HTMLEvents");
  var checked = hint.parent.checked;
  event.initEvent("click", false, true);
  hint.parent.parentElement.dispatchEvent(event);
  if (checked === hint.parent.checked) {
    // fallback if no handler is registered
    hint.parent.checked = !hint.parent.checked;
  }
};

LinkHints.prototype.filterHints = function () {
  var visibleHints = 0;
  for (var i = this.hintsMap.first; i <= this.hintsMap.last; i++) {
    var hint = this.hintsMap[i];
    if (i.toString().startsWith(this.pendingPath)) {
      hint.pendingSpan.innerText   = this.pendingPath;
      hint.remainingSpan.innerText = hint.code.substring(this.pendingPath.length);
      // hint.container.classList.remove("nodisplay"); // for backspace
      visibleHints++;
    } else {
      hint.container.classList.add("nodisplay");
    }
  }
  return visibleHints;
};

function activateLinkHints(linkHintHotKey) {
  if (!linkHintHotKey) return;
  var oLinkHint = new LinkHints(linkHintHotKey);
  document.addEventListener("keypress", oLinkHint.getHandler());
}

/* HOTKEYS */

function Hotkeys(oKeyMap){

  this.oKeyMap = oKeyMap || {};

  // these are the hotkeys provided by the backend
  Object.keys(this.oKeyMap).forEach(function(sKey){

    var action = this.oKeyMap[sKey];

    // add a tooltip/title with the hotkey, currently only sapevents are supported
    this.getAllSapEventsForSapEventName(action).forEach(function(elAnchor) {
      elAnchor.title = elAnchor.title + " [" + sKey + "]";
    });

    // We replace the actions with callback functions to unify
    // the hotkey execution
    this.oKeyMap[sKey] = function(oEvent) {

      // gHelper is only valid for diff page
      var diffHelper = (window.gHelper || {});

      // We have either a js function on this
      if (this[action]) {
        this[action].call(this);
        return;
      }

      // Or a method of the helper object for the diff page
      if (diffHelper[action]){
        diffHelper[action].call(diffHelper);
        return;
      }

      // Or a global function
      if (window[action] && typeof(window[action]) === "function") {
        window[action].call(this);
        return;
      }

      // Or a SAP event link
      var sUiSapEventHref = this.getSapEventHref(action);
      if (sUiSapEventHref) {
        submitSapeventForm({}, sUiSapEventHref, "post");
        oEvent.preventDefault();
        return;
      }

      // Or a SAP event input
      var sUiSapEventInputAction = this.getSapEventInputAction(action);
      if (sUiSapEventInputAction) {
        submitSapeventForm({}, sUiSapEventInputAction, "post");
        oEvent.preventDefault();
        return;
      }

      // Or a SAP event main form
      var elForm = this.getSapEventForm(action);
      if (elForm) {
        elForm.submit();
        oEvent.preventDefault();
        return;
      }

    };

  }.bind(this));

}

Hotkeys.prototype.showHotkeys = function() {
  var elHotkeys = document.querySelector("#hotkeys");

  if (elHotkeys) {
    elHotkeys.style.display = (elHotkeys.style.display) ? "" : "none";
  }
};

Hotkeys.prototype.getAllSapEventsForSapEventName = function(sSapEvent) {
  return [].slice.call(
    document.querySelectorAll('a[href*="sapevent:' + sSapEvent + '"],'
                            + 'a[href*="SAPEVENT:' + sSapEvent + '"],'
                            + 'input[formaction*="sapevent:' + sSapEvent + '"],'
                            + 'input[formaction*="SAPEVENT:' + sSapEvent + '"],'
                            + 'form[action*="sapevent:' + sSapEvent + '"] input[type="submit"].main,'
                            + 'form[action*="SAPEVENT:' + sSapEvent + '"] input[type="submit"].main'));
};

Hotkeys.prototype.getSapEventHref = function(sSapEvent) {

  return this.getAllSapEventsForSapEventName(sSapEvent)
    .filter(function(el){
      // only anchors
      return (!!el.href);
    })
    .map(function(oSapEvent){
      return oSapEvent.href;
    })
    .filter(this.eliminateSapEventFalsePositives(sSapEvent))
    .pop();

};

Hotkeys.prototype.getSapEventInputAction = function(sSapEvent) {

  return this.getAllSapEventsForSapEventName(sSapEvent)
    .filter(function(el){
      // input forms
      return (el.type === "submit");
    })
    .map(function(oSapEvent){
      return oSapEvent.formAction;
    })
    .filter(this.eliminateSapEventFalsePositives(sSapEvent))
    .pop();

};

Hotkeys.prototype.getSapEventForm = function(sSapEvent) {

  return this.getAllSapEventsForSapEventName(sSapEvent)
    .filter(function(el){
      // forms
      var parentForm = el.parentNode.parentNode.parentNode;
      return (el.type === "submit" && parentForm.nodeName === "FORM");
    })
    .map(function(oSapEvent){
      return oSapEvent.parentNode.parentNode.parentNode;
    })
    .pop();

};

Hotkeys.prototype.eliminateSapEventFalsePositives = function(sapEvent){
  return function(sapEventAttr) {
    return sapEventAttr.match(new RegExp("\\b" + sapEvent + "\\b"));
  };
};

Hotkeys.prototype.onkeydown = function(oEvent){

  if (oEvent.defaultPrevented) {
    return;
  }

  if (!Hotkeys.isHotkeyCallPossible()){
    return;
  }

  var
    sKey = oEvent.key || String.fromCharCode(oEvent.keyCode),
    fnHotkey = this.oKeyMap[sKey];

  if (fnHotkey) {
    fnHotkey.call(this, oEvent);
  }
};

Hotkeys.isHotkeyCallPossible = function(){

  var activeElementType = ((document.activeElement && document.activeElement.nodeName) || "");
  var activeElementReadOnly = ((document.activeElement && document.activeElement.readOnly) || false);

  return (activeElementReadOnly || ( activeElementType !== "INPUT" && activeElementType !== "TEXTAREA" ));
};

Hotkeys.addHotkeyToHelpSheet = function(key, description) {
  var hotkeysUl = document.querySelector("#hotkeys ul.hotkeys");
  if (!hotkeysUl) return;

  var li              = document.createElement("li");
  var spanId          = document.createElement("span");
  spanId.className    = "key-id";
  spanId.innerText    = key;
  var spanDescr       = document.createElement("span");
  spanDescr.className = "key-descr";
  spanDescr.innerText = description;
  li.appendChild(spanId);
  li.appendChild(spanDescr);

  hotkeysUl.appendChild(li);
};

function setKeyBindings(oKeyMap){

  var oHotkeys = new Hotkeys(oKeyMap);

  document.addEventListener("keypress", oHotkeys.onkeydown.bind(oHotkeys));
  setTimeout(function(){
    var div = document.getElementById("hotkeys-hint");
    if (div) div.style.opacity = 0.2;
  }, 4900);
  setTimeout(function(){ toggleDisplay("hotkeys-hint") }, 5000);
}

/*
  Patch / git add -p
  */

/*
  We have three type of cascading checkboxes.
  Which means that by clicking a file or section checkbox all corresponding line checkboxes are checked.

  The id of the checkbox indicates its semantics and its membership.

  */

/*
  1) file links

      example id of file link

      patch_file_zcl_abapgit_user_exit.clas.abap
      \________/ \_____________________________/
          |                   |
          |                   |____ file name
          |
          |
          |
      constant prefix

  */

function PatchFile(sId){
  var oRegex = new RegExp("(" + this.ID + ")_(.*$)");
  var oMatch = sId.match(oRegex);
  this.id        = sId;
  this.prefix    = oMatch[1];
  this.file_name = oMatch[2];
}

PatchFile.prototype.ID = "patch_file";

/*
  2) section links within a file

      example id of section link

      patch_section_zcl_abapgit_user_exit.clas.abap_1
      \___________/ \_____________________________/ ^
            |                   |                   |
            |               file name               |
            |                                       |
            |                                       ------ section
            |
      constant prefix

    */

function PatchSection(sId){
  var oRegex = new RegExp("(" + this.ID + ")_(.*)_(\\d+$)");
  var oMatch = sId.match(oRegex);
  this.id        = sId;
  this.prefix    = oMatch[1];
  this.file_name = oMatch[2];
  this.section   = oMatch[3];
}

PatchSection.prototype.ID = "patch_section";

/*
  3) line links within a section

      example id of line link

      patch_line_zcl_abapgit_user_exit.clas.abap_1_25
      \________/ \_____________________________/ ^  ^
            ^                  ^                 |  |
            |                  |                 |  ------- line number
            |               file name            |
            |                                 section
            |
            |
      constant prefix

  */
function PatchLine(){
}

PatchLine.prototype.ID = "patch_line";

function Patch() { }

Patch.prototype.ID = {
  STAGE: "stage"
};

Patch.prototype.ACTION = {
  PATCH_STAGE: "patch_stage",
  REFRESH_LOCAL: "refresh_local",
  REFRESH_ALL: "refresh_all"
};

Patch.prototype.escape = function(sFileName){
  return sFileName
    .replace(/\./g, "\\.")
    .replace(/#/g, "\\#");
};

Patch.prototype.preparePatch = function(){

  this.registerClickHandlerForFiles();
  this.registerClickHandlerForSections();
  this.registerClickHandlerForLines();

};

Patch.prototype.buildSelectorInputStartsWithId = function(sId){
  return "input[id^='" + sId + "']";
};

Patch.prototype.registerClickHandlerForFiles = function(){
  this.registerClickHandlerForSelectorParent(this.buildSelectorInputStartsWithId(PatchFile.prototype.ID), this.onClickFileCheckbox);
};

Patch.prototype.registerClickHandlerForSections = function(){
  this.registerClickHandlerForSelectorParent(this.buildSelectorInputStartsWithId(PatchSection.prototype.ID), this.onClickSectionCheckbox);
};

Patch.prototype.registerClickHandlerForLines = function(){
  this.registerClickHandlerForSelectorParent(this.buildSelectorInputStartsWithId(PatchLine.prototype.ID), this.onClickLineCheckbox);
};

Patch.prototype.registerClickHandlerForSelectorParent = function(sSelector, fnCallback){

  var elAll = document.querySelectorAll(sSelector);

  [].forEach.call(elAll, function(elem){
    elem.parentElement.addEventListener("click", fnCallback.bind(this));
  }.bind(this));

};

Patch.prototype.getAllLineCheckboxesForFile = function(oFile){
  return this.getAllLineCheckboxesForId(oFile.id, PatchFile.prototype.ID);
};

Patch.prototype.getAllSectionCheckboxesForFile = function(oFile){
  return this.getAllSectionCheckboxesForId(oFile.id, PatchFile.prototype.ID);
};

Patch.prototype.getAllLineCheckboxesForSection = function(oSection){
  return this.getAllLineCheckboxesForId(oSection.id, PatchSection.prototype.ID);
};

Patch.prototype.getAllLineCheckboxesForId = function(sId, sIdPrefix){
  return this.getAllCheckboxesForId(sId, sIdPrefix,PatchLine.prototype.ID);
};

Patch.prototype.getAllSectionCheckboxesForId = function(sId, sIdPrefix){
  return this.getAllCheckboxesForId(sId, sIdPrefix, PatchSection.prototype.ID);
};

Patch.prototype.getAllCheckboxesForId = function(sId, sIdPrefix, sNewIdPrefix){
  var oRegex = new RegExp("^" + sIdPrefix);
  sId = sId.replace(oRegex, sNewIdPrefix);
  return document.querySelectorAll(this.buildSelectorInputStartsWithId(this.escape(sId)));
};

Patch.prototype.getToggledCheckbox = function(oEvent){

  var elCheckbox = null;

  // We have either an input element or any element with input child
  // in the latter case we have to toggle the checkbox manually
  if (oEvent.srcElement.nodeName === "INPUT"){
    elCheckbox = oEvent.srcElement;
  } else {
    elCheckbox = this.toggleCheckbox(oEvent.srcElement.querySelector("INPUT"));
  }

  return elCheckbox;
};

Patch.prototype.toggleCheckbox = function(elCheckbox) {
  elCheckbox.checked = !elCheckbox.checked;
  return elCheckbox;
};

Patch.prototype.onClickFileCheckbox = function(oEvent) {

  var elCheckbox = this.getToggledCheckbox(oEvent);
  var oFile = new PatchFile(elCheckbox.id);
  var elAllLineCheckboxesOfFile = this.getAllLineCheckboxesForFile(oFile);
  var elAllSectionCheckboxesOfFile = this.getAllSectionCheckboxesForFile(oFile);

  [].forEach.call(elAllLineCheckboxesOfFile,function(elem){
    elem.checked = elCheckbox.checked;
  }.bind(this));

  [].forEach.call(elAllSectionCheckboxesOfFile,function(elem){
    elem.checked = elCheckbox.checked;
  }.bind(this));

};

Patch.prototype.onClickSectionCheckbox = function(oEvent){
  var elSrcElement = this.getToggledCheckbox(oEvent);
  var oSection = new PatchSection(elSrcElement.id);
  this.clickAllLineCheckboxesInSection(oSection, elSrcElement.checked);
};

Patch.prototype.onClickLineCheckbox = function(oEvent){
  this.getToggledCheckbox(oEvent);
};

Patch.prototype.clickAllLineCheckboxesInSection = function(oSection, bChecked){

  var elAllLineCheckboxesOfSection = this.getAllLineCheckboxesForSection(oSection);

  [].forEach.call(elAllLineCheckboxesOfSection,function(elem){
    elem.checked = bChecked;
  }.bind(this));

};

Patch.prototype.registerStagePatch = function (){

  var elStage = document.querySelector("#" + this.ID.STAGE);
  var REFRESH_PREFIX = "refresh";

  elStage.addEventListener("click", this.submitPatch.bind(this, this.ACTION.PATCH_STAGE));

  var aRefresh = document.querySelectorAll("[id*=" + REFRESH_PREFIX + "]");
  [].forEach.call( aRefresh, function(el) {
    el.addEventListener("click", memoizeScrollPosition(this.submitPatch.bind(this, el.id)).bind(this));
  }.bind(this));

  // for hotkeys
  window.stagePatch = function(){
    this.submitPatch(this.ACTION.PATCH_STAGE);
  }.bind(this);

  window.refreshLocal = memoizeScrollPosition(function(){
    this.submitPatch(this.ACTION.REFRESH_LOCAL);
  }.bind(this));

  window.refreshAll = memoizeScrollPosition(function(){
    this.submitPatch(this.ACTION.REFRESH_ALL);
  }.bind(this));

};

Patch.prototype.submitPatch = function(action) {
  // Collect add and remove info and submit to backend

  var aAddPatch = this.collectElementsForCheckboxId( PatchLine.prototype.ID, true);
  var aRemovePatch = this.collectElementsForCheckboxId( PatchLine.prototype.ID, false);

  submitSapeventForm({ add: aAddPatch, remove: aRemovePatch }, action, "post");
};

Patch.prototype.collectElementsForCheckboxId = function(sId, bChecked){

  var sSelector = this.buildSelectorInputStartsWithId(sId);

  return [].slice.call(document.querySelectorAll(sSelector))
    .filter(function(elem){
      return (elem.checked === bChecked);
    }).map(function(elem){
      return elem.id;
    });

};

function preparePatch(){
  var oPatch = new Patch();
  oPatch.preparePatch();
}

function registerStagePatch(){
  var oPatch = new Patch();
  oPatch.registerStagePatch();
}

/**********************************************************
 * Page branch overview
 *
 * Hovering a commit node in the branch overview will show
 * a popup with the commit details. Single click on a node
 * will fix the popup, so that users can select text. The
 * fixation is removed when any node is hovered or the popup
 * is closed via 'X'.
 *
 **********************************************************/

function BranchOverview() {
  this.bFixed = false;
  this.elCurrentCommit = {
    style : {
      display: "none"
    }
  };
}

BranchOverview.prototype.toggleCommit = function(sSha1, bFixPopup) {

  // If the popup is fixed, we just remove the fixation.
  // The popup will then be hidden by the next call of hideCommit
  if (this.bFixed) {
    this.bFixed = false;
    return;
  }

  // We hide the previous shown commit popup
  this.elCurrentCommit.style.display = "none";

  // Display the new commit popup if sha1 is supplied
  if (sSha1){
    this.elCurrentCommit = document.getElementById(sSha1);
    this.elCurrentCommit.style.display = "";

    // and fix the popup so that the next hideCommit won't hide it.
    this.bFixed = bFixPopup;

  }

};

// called by onClick of commit nodes in branch overview
BranchOverview.prototype.onCommitClick = function(commit){
  this.toggleCommit(commit.sha1, true);
};

// Called by commit:mouseover
BranchOverview.prototype.showCommit = function(event){
  this.toggleCommit(event.data.sha1);
};

// Called by commit:mouseout
BranchOverview.prototype.hideCommit = function (){
  this.toggleCommit();
};

// Initialize Top Horizontal Scroller on GitGraph
function setGitGraphScroller(){ // eslint-disable-line no-unused-vars

  // Get gitGraph Element Canvas Width
  var gitGraphEl = document.getElementById("gitGraph");
  var gitGraphWidth = gitGraphEl.offsetWidth;

  // Initialize gitGraph-HTopScroller Element width as gitGraph
  var HTopScrollerEl = document.querySelector(".gitGraph-HTopScroller");
  HTopScrollerEl.style.width = gitGraphWidth + "px";

}

// Setup Top Horizontal Scroller on GitGraph event
function GitGraphScroller() { // eslint-disable-line no-unused-vars
  var gitGraphWrapperEl = document.querySelector(".gitGraph-Wrapper");
  var gitGraphscrollWrapperEl = document.querySelector(".gitGraph-scrollWrapper");
  gitGraphWrapperEl.scrollLeft = gitGraphscrollWrapperEl.scrollLeft;
}

/**********************************************************
 * Ctrl + P - command palette
 **********************************************************/

// fuzzy match helper
// return non empty marked string in case it fits the filter
// abc + b = a<mark>b</mark>c
function fuzzyMatchAndMark(str, filter){
  var markedStr   = "";
  var filterLower = filter.toLowerCase();
  var strLower    = str.toLowerCase();
  var cur         = 0;

  for (var i = 0; i < filter.length; i++) {
    while (filterLower[i] !== strLower[cur] && cur < str.length) {
      markedStr += str[cur++];
    }
    if (cur === str.length) break;
    markedStr += "<mark>" + str[cur++] + "</mark>";
  }

  var matched = i === filter.length;
  if (matched && cur < str.length) markedStr += str.substring(cur);
  return matched ? markedStr : null;
}

function CommandPalette(commandEnumerator, opts) {
  if (typeof commandEnumerator !== "function") throw Error("commandEnumerator must be a function");
  if (typeof opts !== "object") throw Error("opts must be an object");
  if (typeof opts.toggleKey !== "string" || !opts.toggleKey) throw Error("toggleKey must be a string");
  this.commands = commandEnumerator();
  if (!this.commands) return;
  // this.commands = [{
  //   action:    "sap_event_action_code_with_params"
  //   iconClass: "icon icon_x ..."
  //   title:     "my command X"
  // }, ...];

  if (opts.toggleKey[0] === "^") {
    this.toggleKeyCtrl = true;
    this.toggleKey     = opts.toggleKey.substring(1);
    if (!this.toggleKey) throw Error("Incorrect toggleKey");
  } else {
    this.toggleKeyCtrl = false;
    this.toggleKey     = opts.toggleKey;
  }

  this.hotkeyDescription = opts.hotkeyDescription;
  this.elements = {
    palette: null,
    ul:      null,
    input:   null
  };
  this.selectIndex       = -1; // not selected
  this.filter            = "";
  this.renderAndBindElements();
  this.hookEvents();
  Hotkeys.addHotkeyToHelpSheet(opts.toggleKey, opts.hotkeyDescription);

  if (!CommandPalette.instances) {
    CommandPalette.instances = [];
  }
  CommandPalette.instances.push(this);
}

CommandPalette.prototype.hookEvents = function(){
  document.addEventListener("keydown", this.handleToggleKey.bind(this));
  this.elements.input.addEventListener("keyup", this.handleInputKey.bind(this));
  this.elements.ul.addEventListener("click", this.handleUlClick.bind(this));
};

CommandPalette.prototype.renderCommandItem = function(cmd){
  var li = document.createElement("li");
  if (cmd.iconClass) {
    var icon       = document.createElement("i");
    icon.className = cmd.iconClass;
    li.appendChild(icon);
  }
  var titleSpan = document.createElement("span");
  li.appendChild(titleSpan);
  cmd.element   = li;
  cmd.titleSpan = titleSpan;
  return li;
};

CommandPalette.prototype.renderAndBindElements = function(){
  var div           = document.createElement("div");
  div.className     = "cmd-palette";
  div.style.display = "none";
  var input         = document.createElement("input");
  input.placeholder = this.hotkeyDescription;
  var ul            = document.createElement("ul");
  for (var i = 0; i < this.commands.length; i++) ul.appendChild(this.renderCommandItem(this.commands[i]));
  div.appendChild(input);
  div.appendChild(ul);

  this.elements.palette = div;
  this.elements.input   = input;
  this.elements.ul      = ul;
  document.body.appendChild(div);
};

CommandPalette.prototype.handleToggleKey = function(event){
  if (event.key !== this.toggleKey) return;
  if (this.toggleKeyCtrl && !event.ctrlKey) return;
  this.toggleDisplay();
  event.preventDefault();
};

CommandPalette.prototype.handleInputKey = function(event){
  if (event.key === "ArrowUp" || event.key === "Up") {
    this.selectPrev();
  } else if (event.key === "ArrowDown" || event.key === "Down") {
    this.selectNext();
  } else if (event.key === "Enter") {
    this.exec(this.getSelected());
  } else if (event.key === "Backspace" && !this.filter) {
    this.toggleDisplay(false);
  } else if (this.filter !== this.elements.input.value) {
    this.filter = this.elements.input.value;
    this.applyFilter();
    this.selectFirst();
  }
  event.preventDefault();
};

CommandPalette.prototype.applyFilter = function(){
  for (var i = 0; i < this.commands.length; i++) {
    var cmd = this.commands[i];
    if (!this.filter) {
      cmd.element.style.display = "";
      cmd.titleSpan.innerText   = cmd.title;
    } else {
      var matchedTitle = fuzzyMatchAndMark(cmd.title, this.filter);
      if (matchedTitle) {
        cmd.titleSpan.innerHTML   = matchedTitle;
        cmd.element.style.display = "";
      } else {
        cmd.element.style.display = "none";
      }
    }
  }
};

CommandPalette.prototype.applySelectIndex = function(newIndex){
  if (newIndex !== this.selectIndex) {
    if (this.selectIndex >= 0) this.commands[this.selectIndex].element.classList.remove("selected");
    var newCmd = this.commands[newIndex];
    newCmd.element.classList.add("selected");
    this.selectIndex = newIndex;
    this.adjustScrollPosition(newCmd.element);
  }
};

CommandPalette.prototype.selectFirst = function(){
  for (var i = 0; i < this.commands.length; i++) {
    if (this.commands[i].element.style.display === "none") continue; // skip hidden
    this.applySelectIndex(i);
    break;
  }
};

CommandPalette.prototype.selectNext = function(){
  for (var i = this.selectIndex + 1; i < this.commands.length; i++) {
    if (this.commands[i].element.style.display === "none") continue; // skip hidden
    this.applySelectIndex(i);
    break;
  }
};

CommandPalette.prototype.selectPrev = function(){
  for (var i = this.selectIndex - 1; i >= 0; i--) {
    if (this.commands[i].element.style.display === "none") continue; // skip hidden
    this.applySelectIndex(i);
    break;
  }
};

CommandPalette.prototype.getSelected = function(){
  return this.commands[this.selectIndex];
};

CommandPalette.prototype.adjustScrollPosition = function(itemElement){
  var bItem         = itemElement.getBoundingClientRect();
  var bContainer    = this.elements.ul.getBoundingClientRect();
  bItem.top         = Math.round(bItem.top);
  bItem.bottom      = Math.round(bItem.bottom);
  bItem.height      = Math.round(bItem.height);
  bItem.mid         = Math.round(bItem.top + bItem.height / 2);
  bContainer.top    = Math.round(bContainer.top);
  bContainer.bottom = Math.round(bContainer.bottom);

  if ( bItem.mid > bContainer.bottom - 2 ) {
    this.elements.ul.scrollTop += bItem.bottom - bContainer.bottom;
  } else if ( bItem.mid < bContainer.top + 2 ) {
    this.elements.ul.scrollTop += bItem.top - bContainer.top;
  }
};

CommandPalette.prototype.toggleDisplay = function(forceState) {
  var isDisplayed = (this.elements.palette.style.display !== "none");
  var tobeDisplayed = (forceState !== undefined) ? forceState : !isDisplayed;

  if (tobeDisplayed) {
    // auto close other command palettes
    CommandPalette.instances.forEach(function(instance){
      instance.elements.palette.style.display = "none";
    });
  }

  this.elements.palette.style.display = tobeDisplayed ? "" : "none";
  if (tobeDisplayed) {
    this.elements.input.value = "";
    this.elements.input.focus();
    this.applyFilter();
    this.selectFirst();
  }

};

CommandPalette.prototype.getCommandByElement = function(element) {
  for (var i = 0; i < this.commands.length; i++) {
    if (this.commands[i].element === element) return this.commands[i];
  }
};

CommandPalette.prototype.handleUlClick = function(event) {
  var element = event.target || event.srcElement;
  if (!element) return;
  if (element.nodeName === "SPAN") element = element.parentNode;
  if (element.nodeName === "I") element = element.parentNode;
  if (element.nodeName !== "LI") return;
  this.exec(this.getCommandByElement(element));
};

CommandPalette.prototype.exec = function(cmd) {
  if (!cmd) return;
  this.toggleDisplay(false);
  if (typeof cmd.action === "function"){
    cmd.action();
  } else {
    submitSapeventForm(null, cmd.action);
  }
};

/* COMMAND ENUMERATORS */

function createRepoCatalogEnumerator(catalog, action) {
  // expecting [{ key, isOffline, displayName }]
  return function() {
    return catalog.map(function(i) {
      return {
        action:    action + "?key=" + i.key,
        iconClass: i.isOffline
          ? "icon icon-plug darkgrey"
          : "icon icon-cloud-upload-alt blue",
        title: i.displayName
      };
    });
  };
}

function enumerateUiActions() {

  var items = [];
  function processUL(ulNode, prefix) {
    for (var i = 0; i < ulNode.children.length; i++) {
      var item = ulNode.children[i];
      if (item.nodeName !== "LI") continue; // unexpected node
      if (item.children.length >=2 && item.children[1].nodeName === "UL") {
        // submenu detected
        var menutext = item.children[0].innerText;
        // special treatment for menus without text
        if (!menutext) {
          menutext = item.children[0].getAttribute("title");
        }
        processUL(item.children[1], menutext);
      } else if (item.firstElementChild && item.firstElementChild.nodeName === "A") {
        var anchor = item.firstElementChild;
        if (anchor.href && anchor.href !== "#") items.push([anchor, prefix]);
      }
    }
  }

  // toolbars
  [].slice.call(document.querySelectorAll("[id*=toolbar]"))
    .filter(function(toolbar){
      return (toolbar && toolbar.nodeName === "UL");
    }).forEach(function(toolbar){
      processUL(toolbar);
    });

  items = items.map(function(item) {
    var action = "";
    var anchor = item[0];
    if (anchor.href.includes("#")) {
      action = function(){
        anchor.click();
      };
    } else {
      action = anchor.href.replace("sapevent:", "");
    }
    var prefix = item[1];
    return {
      action:    action,
      title:     (prefix ? prefix + ": " : "") + anchor.innerText.trim()
    };
  });

  // forms
  [].slice.call(document.querySelectorAll("input[type='submit']"))
    .forEach(function(input){
      items.push({
        action: function(){
          if ([].slice.call(input.classList).indexOf("main") !== -1){
            var parentForm = input.parentNode.parentNode.parentNode;
            if (parentForm.nodeName === "FORM"){
              parentForm.submit();
            }
          } else {
            submitSapeventForm({}, input.formAction, "post");
          }
        },
        title: input.value + " " + input.title.replace(/\[.*\]/,"")
      });
    });

  // links inside forms
  [].slice.call(document.querySelectorAll("form a"))
    .filter(function(anchor){
      return !!anchor.title;
    }).forEach(function(anchor){
      items.push({
        action: function(){
          anchor.click();
        },
        title: anchor.title
      });
    });

  return items;
}

function enumerateJumpAllFiles() {
  var root = document.getElementById("jump");
  if (!root || root.nodeName !== "UL") return null;

  return Array
    .prototype.slice.call(root.children)
    .filter(function(elem) { return elem.nodeName === "LI" })
    .map(function(listItem) {
      var title = listItem.children[0].childNodes[0].textContent;
      return {
        action: root.onclick.bind(null, title),
        title:  title
      };});
}

/* Save Scroll Position for Diff/Patch Page */

function saveScrollPosition(){
  if (!window.sessionStorage) { return }
  window.sessionStorage.setItem("scrollTop", document.querySelector("html").scrollTop);
}

function restoreScrollPosition(){
  if (!window.sessionStorage) { return }

  var scrollTop = window.sessionStorage.getItem("scrollTop");
  if (scrollTop) {
    document.querySelector("html").scrollTop = scrollTop;
  }
  window.sessionStorage.setItem("scrollTop", 0);
}

function memoizeScrollPosition(fn){
  return function(){
    saveScrollPosition();
    return fn.call(this, fn.args);
  }.bind(this);
}

/* STICKY HEADERS */

/* https://www.w3schools.com/howto/howto_js_navbar_sticky.asp */
/* Note: We have to use JS since IE does not support CSS position:sticky */

// When the user scrolls the page, execute toggleSticky
window.onscroll = function() { toggleSticky() };

// Add the sticky class to the navbar when you reach its scroll position.
// Remove "sticky" when you leave the scroll position
function toggleSticky() {
  var body = document.getElementsByTagName("body")[0];
  var header = document.getElementById("header");
  var sticky = header.offsetTop;

  var stickyClass = "sticky";
  if (body.classList.contains("full_width")) {
    stickyClass = "sticky_full_width";
  }

  if (window.pageYOffset >= sticky) {
    header.classList.add( stickyClass );
  } else {
    header.classList.remove( stickyClass );
  }
}
