/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

const table = document.getElementById("table");

function setTextContent(element, content) {
  if (content.startsWith("customkeys-")) {
    element.setAttribute("data-l10n-id", content);
  } else {
    element.textContent = content;
  }
}

function notifyUpdate() {
  window.dispatchEvent(new CustomEvent("CustomKeysUpdate"));
}

async function buildTable() {
  const keys = await RPMSendQuery("CustomKeys:GetKeys");
  for (const category in keys) {
    const tbody = document.createElement("tbody");
    table.append(tbody);
    let row = document.createElement("tr");
    row.className = "category";
    tbody.append(row);
    let cell = document.createElement("td");
    row.append(cell);
    cell.setAttribute("colspan", 5);
    const heading = document.createElement("h1");
    setTextContent(heading, category);
    cell.append(heading);
    const categoryKeys = keys[category];
    for (const keyId in categoryKeys) {
      row = document.createElement("tr");
      row.className = "key";
      tbody.append(row);
      row.setAttribute("data-id", keyId);
      cell = document.createElement("th");
      const key = categoryKeys[keyId];
      setTextContent(cell, key.title);
      row.append(cell);
      cell = document.createElement("td");
      cell.textContent = key.shortcut;
      row.append(cell);
      cell = document.createElement("td");
      let button = document.createElement("button");
      button.className = "change";
      button.setAttribute("data-l10n-id", "customkeys-change");
      cell.append(button);
      let label = document.createElement("label");
      label.className = "newLabel";
      let span = document.createElement("span");
      span.setAttribute("data-l10n-id", "customkeys-new-key");
      label.append(span);
      let input = document.createElement("input");
      input.className = "new";
      label.append(input);
      cell.append(label);
      row.append(cell);
      cell = document.createElement("td");
      button = document.createElement("button");
      button.className = "clear";
      button.setAttribute("data-l10n-id", "customkeys-clear");
      cell.append(button);
      row.append(cell);
      cell = document.createElement("td");
      button = document.createElement("button");
      button.className = "reset";
      button.setAttribute("data-l10n-id", "customkeys-reset");
      cell.append(button);
      row.append(cell);
      updateKey(row, key);
    }
  }
  notifyUpdate();
}

function updateKey(row, data) {
  row.children[1].textContent = data.shortcut;
  row.classList.toggle("customized", data.isCustomized);
  row.classList.toggle("assigned", !!data.shortcut);
}

// Returns false if the assignment should be cancelled.
async function maybeHandleConflict(data) {
  for (const row of table.querySelectorAll(".key")) {
    if (data.shortcut != row.children[1].textContent) {
      continue; // Not a conflict.
    }
    const conflictId = row.dataset.id;
    if (conflictId == data.id) {
      // We're trying to assign this key to the shortcut it is already
      // assigned to. We don't need to do anything.
      return false;
    }
    const conflictDesc = row.children[0].textContent;
    if (
      window.confirm(
        await document.l10n.formatValue("customkeys-conflict-confirm", {
          conflict: conflictDesc,
        })
      )
    ) {
      // Clear the conflicting key.
      const newData = await RPMSendQuery("CustomKeys:ClearKey", conflictId);
      updateKey(row, newData);
      return true;
    }
    return false;
  }
  return true;
}

async function onAction(event) {
  const row = event.target.closest("tr");
  const keyId = row.dataset.id;
  if (event.target.className == "reset") {
    Glean.browserCustomkeys.actions.reset.add();
    const data = await RPMSendQuery("CustomKeys:GetDefaultKey", keyId);
    if (await maybeHandleConflict(data)) {
      const newData = await RPMSendQuery("CustomKeys:ResetKey", keyId);
      updateKey(row, newData);
      notifyUpdate();
    }
  } else if (event.target.className == "change") {
    Glean.browserCustomkeys.actions.change.add();
    // The "editing" class will cause the Change button to be replaced by a
    // labelled input for the new key.
    row.classList.add("editing");
    // We need to listen for keys in the parent process because we want to
    // intercept reserved keys, which we can't do in the content process.
    RPMSendAsyncMessage("CustomKeys:CaptureKey", true);
    row.querySelector(".new").focus();
  } else if (event.target.className == "clear") {
    Glean.browserCustomkeys.actions.clear.add();
    const newData = await RPMSendQuery("CustomKeys:ClearKey", keyId);
    updateKey(row, newData);
    notifyUpdate();
  }
}

async function onKey({ data }) {
  const input = document.activeElement;
  const row = input.closest("tr");
  data.id = row.dataset.id;
  if (data.isModifier) {
    // This is a modifier. Display it, but don't assign yet. We assign when the
    // main key is pressed (below).
    input.value = data.modifierString;
    // Select the input's text so screen readers will report it.
    input.select();
    return;
  }
  if (await maybeHandleConflict(data)) {
    const newData = await RPMSendQuery("CustomKeys:ChangeKey", data);
    updateKey(row, newData);
  }
  RPMSendAsyncMessage("CustomKeys:CaptureKey", false);
  row.classList.remove("editing");
  row.querySelector(".change").focus();
  notifyUpdate();
}

function onFocusLost(event) {
  if (event.target.className == "new") {
    // If the input loses focus, cancel editing of the key.
    RPMSendAsyncMessage("CustomKeys:CaptureKey", false);
    const row = event.target.closest("tr");
    row.classList.remove("editing");
    // Clear any modifiers that were displayed, ready for the next edit.
    event.target.value = "";
  }
}

function onSearchInput(event) {
  const query = event.target.value.toLowerCase();
  for (const row of table.querySelectorAll(".key")) {
    row.hidden =
      query && !row.children[0].textContent.toLowerCase().includes(query);
  }
  for (const tbody of table.tBodies) {
    // Show a category only if it has at least 1 shown key.
    tbody.hidden = !tbody.querySelector(".key:not([hidden])");
  }
  notifyUpdate();
}

async function onResetAll() {
  Glean.browserCustomkeys.actions.reset_all.add();
  if (
    !window.confirm(
      await document.l10n.formatValue("customkeys-reset-all-confirm")
    )
  ) {
    return;
  }
  await RPMSendQuery("CustomKeys:ResetAll");
  const keysByCat = await RPMSendQuery("CustomKeys:GetKeys");
  const keysById = {};
  for (const category in keysByCat) {
    const categoryKeys = keysByCat[category];
    for (const keyId in categoryKeys) {
      keysById[keyId] = categoryKeys[keyId];
    }
  }
  for (const row of table.querySelectorAll(".key")) {
    const data = keysById[row.dataset.id];
    if (data) {
      updateKey(row, data);
    }
  }
  notifyUpdate();
}

buildTable();
table.addEventListener("click", onAction);
RPMAddMessageListener("CustomKeys:CapturedKey", onKey);
table.addEventListener("focusout", onFocusLost);
document.getElementById("search").addEventListener("input", onSearchInput);
document.getElementById("resetAll").addEventListener("click", onResetAll);
Glean.browserCustomkeys.opened.add();
