(function () {
  "use strict";

  var REF_TOKEN =
    /(?:^|[\s(])(#([0-9a-fA-F]{4,40})|@([A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*))/g;
  var SUGGEST_LIMIT = 12;

  function projectPath(project) {
    return (
      "/project/" +
      (project || "")
        .split("/")
        .map(function (segment) {
          return encodeURIComponent(segment);
        })
        .join("/")
    );
  }

  function escapeHtml(value) {
    return String(value)
      .replace(/&/g, "&")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  }

  function splitReferenceText(text) {
    var parts = [];
    var last = 0;
    var match;
    REF_TOKEN.lastIndex = 0;
    while ((match = REF_TOKEN.exec(text)) !== null) {
      var full = match[0];
      var lead = full.charAt(0);
      var token = lead === "#" || lead === "@" ? full : full.slice(1);
      var tokenStart = match.index + full.length - token.length;
      if (tokenStart > last) {
        parts.push({ type: "text", value: text.slice(last, tokenStart) });
      }
      if (token.charAt(0) === "#") {
        parts.push({ type: "commit", value: token.slice(1) });
      } else {
        parts.push({ type: "ref", value: token.slice(1) });
      }
      last = match.index + full.length;
    }
    if (last < text.length) {
      parts.push({ type: "text", value: text.slice(last) });
    }
    return parts;
  }

  function shouldSkipTextNode(node) {
    var parent = node.parentElement;
    if (!parent) return true;
    if (parent.closest("pre, code, a, .board-ref-link")) return true;
    return false;
  }

  function resolveCommit(refId, manifest) {
	if (!refId || !manifest || !manifest.commits) return null;
    var needle = refId.toLowerCase();
    var i;
    var entry;
    var parts;
    var p;
    for (i = 0; i < manifest.commits.length; i++) {
      entry = manifest.commits[i];
      if (entry.insert.toLowerCase() === needle) return entry;
      parts = (entry.tags || "").split(/\s+/);
      for (p = 0; p < parts.length; p++) {
        if (parts[p].indexOf(needle) === 0) return entry;
      }
    }
    return null;
  }

  function resolveRef(refId, manifest) {
    if (!refId || !manifest || !manifest.refs) return null;
    var needle = refId.toLowerCase();
    var i;
    var entry;
    for (i = 0; i < manifest.refs.length; i++) {
      entry = manifest.refs[i];
      if (entry.insert.toLowerCase() === needle) return entry;
    }
    if (/^\d+$/.test(refId) && refId.length >= 4) {
      for (i = 0; i < manifest.refs.length; i++) {
        entry = manifest.refs[i];
        if (entry.insert.slice(-refId.length) === refId) return entry;
      }
    }
    for (i = 0; i < manifest.refs.length; i++) {
      entry = manifest.refs[i];
      if (entry.tags.indexOf(needle) !== -1) return entry;
    }
    return null;
  }

  function linkifyElement(root, manifest, project) {
    if (!root || !project) return;
    var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
    var textNodes = [];
    var node;
    while ((node = walker.nextNode())) {
      if (!shouldSkipTextNode(node)) textNodes.push(node);
    }
    textNodes.forEach(function (textNode) {
      var text = textNode.textContent || "";
      var parts = splitReferenceText(text);
      if (parts.length <= 1 && parts[0] && parts[0].type === "text") return;
      var frag = document.createDocumentFragment();
      parts.forEach(function (part) {
        if (part.type === "text") {
          frag.appendChild(document.createTextNode(part.value));
          return;
        }
        if (part.type === "commit") {
          var resolvedCommit = resolveCommit(part.value, manifest);
          var commitHref =
            (resolvedCommit && resolvedCommit.url) ||
            projectPath(project) + "?a=commit&h=" + encodeURIComponent(part.value);
          var commitLink = document.createElement("a");
          commitLink.href = commitHref;
          commitLink.className = "board-ref-link board-ref-commit";
          commitLink.textContent = "#" + part.value;
          if (resolvedCommit) commitLink.title = resolvedCommit.name;
          frag.appendChild(commitLink);
          return;
        }
        var resolved = resolveRef(part.value, manifest);
        if (resolved) {
          var refLink = document.createElement("a");
          refLink.className = "board-ref-link board-ref-tag";
          refLink.textContent = "@" + part.value;
          refLink.href = resolved.url;
          refLink.title = resolved.name;
          frag.appendChild(refLink);
        } else {
          var unresolved = document.createElement("span");
          unresolved.className = "board-ref-link board-ref-unresolved";
          unresolved.textContent = "@" + part.value;
          unresolved.title = "Unresolved reference";
          frag.appendChild(unresolved);
        }
      });
      textNode.parentNode.replaceChild(frag, textNode);
    });
  }

  function filterManifestItems(items, query) {
    var list = items || [];
    var q = (query || "").toLowerCase();
    if (!q) return list.slice(0, SUGGEST_LIMIT);
    return list
      .filter(function (item) {
        return (
          item.name.toLowerCase().indexOf(q) !== -1 || (item.tags || "").indexOf(q) !== -1
        );
      })
      .slice(0, SUGGEST_LIMIT);
  }

  function loadManifest(project, board) {
    var url =
      projectPath(project) +
      "/board-ref-manifest?" +
      new URLSearchParams({ b: board || "Tasks" }).toString();
    return fetch(url)
      .then(function (res) {
        return res.ok ? res.json() : { commits: [], refs: [] };
      })
      .then(function (data) {
        return {
          commits: Array.isArray(data.commits) ? data.commits : [],
          refs: Array.isArray(data.refs) ? data.refs : [],
        };
      })
      .catch(function () {
        return { commits: [], refs: [] };
      });
  }

  function attachAutocomplete(textarea, manifest) {
    if (!textarea || textarea.dataset.boardRefAutocomplete === "1") return;
    textarea.dataset.boardRefAutocomplete = "1";
    var menu = document.createElement("div");
    menu.className = "board-ref-suggest d-none";
    document.body.appendChild(menu);

    var activeTrigger = null;
    var activeStart = -1;

    function hideMenu() {
      menu.classList.remove("show");
      menu.classList.add("d-none");
      menu.innerHTML = "";
      activeTrigger = null;
      activeStart = -1;
    }

    function positionMenu() {
      var rect = textarea.getBoundingClientRect();
      menu.style.left = rect.left + window.scrollX + "px";
      menu.style.top = rect.bottom + window.scrollY + 4 + "px";
      menu.style.minWidth = Math.max(rect.width, 280) + "px";
    }

    function currentToken() {
      var value = textarea.value;
      var pos = textarea.selectionStart || 0;
      var before = value.slice(0, pos);
      var match = before.match(/(^|[\s(])((@[A-Za-z0-9._/-]*)|(#[0-9a-fA-F]*))$/);
      if (!match) return null;
      var token = match[2];
      return {
        trigger: token.charAt(0),
        query: token.slice(1),
        start: before.length - token.length,
      };
    }

    function renderMenu(items) {
      menu.innerHTML = "";
      if (!items.length) {
        hideMenu();
        return;
      }
      items.forEach(function (item) {
        var row = document.createElement("button");
        row.type = "button";
        row.className = "board-ref-suggest-item";
        row.innerHTML =
          '<div class="board-ref-suggest-title">' +
          escapeHtml(item.name) +
          "</div>" +
          '<div class="board-ref-suggest-snippet">' +
          escapeHtml(item.snippet || item.insert || "") +
          "</div>" +
          (item.created
            ? '<div class="board-ref-suggest-date">' + escapeHtml(item.created) + "</div>"
            : "");
        row.addEventListener("mousedown", function (e) {
          e.preventDefault();
          if (activeStart < 0 || !activeTrigger) return;
          var insert = activeTrigger + item.insert;
          var value = textarea.value;
          var end = textarea.selectionStart || 0;
          textarea.value = value.slice(0, activeStart) + insert + value.slice(end);
          var caret = activeStart + insert.length;
          textarea.setSelectionRange(caret, caret);
          hideMenu();
          textarea.dispatchEvent(new Event("input", { bubbles: true }));
        });
        menu.appendChild(row);
      });
      positionMenu();
      menu.classList.remove("d-none");
      menu.classList.add("show");
    }

    function refreshMenu() {
      var token = currentToken();
      if (!token) {
        hideMenu();
        return;
      }
      activeTrigger = token.trigger;
      activeStart = token.start;
      var items =
        token.trigger === "#"
          ? filterManifestItems(manifest.commits, token.query)
          : filterManifestItems(manifest.refs, token.query);
      renderMenu(items);
    }

    textarea.addEventListener("input", refreshMenu);
    textarea.addEventListener("keyup", refreshMenu);
    textarea.addEventListener("blur", function () {
      window.setTimeout(hideMenu, 150);
    });
    textarea.addEventListener("keydown", function (e) {
      if (e.key === "Escape") hideMenu();
    });

    document.addEventListener("mousedown", function (e) {
      if (menu.classList.contains("d-none")) return;
      if (menu.contains(e.target) || textarea.contains(e.target)) return;
      hideMenu();
    });
  }

  window.BoardRefs = {
    loadManifest: loadManifest,
    linkifyElement: linkifyElement,
    attachAutocomplete: attachAutocomplete,
    resolveRef: resolveRef,
    resolveCommit: resolveCommit,
    filterManifestItems: filterManifestItems,
  };
})();