"use strict";

/* Tags menu filter */

// Get a dictionary of tag name -> tag link from HTML
function getTags() {
  const tags = {};
  const list = document.querySelectorAll('.versions a');
  for (const el of list) {
    tags[el.innerText.trim()] = el.href;
  }
  return tags;
}

// Generate tag search results based on input
// filter: current filter input text
// tags: dictionary of tag name -> tag link
function generateResults(filter, tags) {
  const searchResults = document.createDocumentFragment();
  const filterRegex = new RegExp(filter, 'i');

  for (let key in tags) {
    if (tags.hasOwnProperty(key)) {
      let tagFound = false;
      const tagHighlight = key.replace(filterRegex, result => {
        if (result) tagFound = true;
        return '<strong>' + result + '</strong>';
      })

      if (tagFound) {
        const tagLink = document.createElement('a');
        tagLink.href = tags[key];
        tagLink.innerHTML = tagHighlight;
        searchResults.appendChild(tagLink);
      }
    }
  }

  return searchResults;
}

// Setup tags filter input
function setupVersionsFilter() {
  const input = document.querySelector('.filter-input');
  const results = document.querySelector('.filter-results');
  const versions = document.querySelector('.versions');
  const tags = getTags();

  input.addEventListener('input', e => {
    if (e.target.value === '') {
      versions.classList.remove('hide');
      results.innerHTML = '';
    } else {
      versions.classList.add('hide');
      results.innerHTML = '';
      results.appendChild(generateResults(e.target.value, tags));
    }
  });
}

// Setup expanding/collapsing versions tree on click
function setupVersionsTree() {
  const versions = document.querySelector('.versions');
  versions.addEventListener('click', e => {
    if (e.target && e.target.nodeName == 'SPAN') {
      e.target.classList.toggle('active')
    }
  });
}

function isWidescreen() {
  return getComputedStyle(document.documentElement).getPropertyValue('--is-widescreen') === 'true';
}

// Toggles sidebar visibility, handles widescreen and mobile layouts
function toggleMenu() {
  const isWidescreen = getComputedStyle(document.documentElement).getPropertyValue('--is-widescreen') === 'true';
  if(isWidescreen) {
    const hasShowMenu = document.documentElement.classList.contains('show-menu');
    window.localStorage.setItem('show-sidebar', !hasShowMenu);
    document.documentElement.classList.toggle('show-menu');
  } else {
    document.documentElement.classList.toggle('show-menu-mobile');
  }
}

// Setup sidebar hamburger menu button, close button and mobile sidebar backdrop events
function setupSidebarSwitch() {
  const tag = document.querySelector('.version em');
  const openMenu = document.querySelector('.open-menu');
  const sidebar = document.querySelector('.sidebar');

  // toggle on hamburger menu click
  openMenu.addEventListener('click', e => {
    e.preventDefault();
    toggleMenu();
  });

  // toggle on footer tag icon click
  tag.addEventListener('click', e => {
    e.preventDefault();
    toggleMenu();
  });

  // close on close-menu/backdrop click
  sidebar.addEventListener('click', e => {
    if (e.target === sidebar && isWidescreen()) {
      document.documentElement.classList.remove('show-menu');
      window.localStorage.setItem('show-sidebar', false);
    } else if (e.target === sidebar || e.target.classList.contains('close-menu')) {
      document.documentElement.classList.remove('show-menu-mobile');
    }
  });
}

// Parse and validate line identifier in format L${number}
function parseLineId(lineId) {
  if (lineId[0] != "L") {
    return;
  }

  let lineIdNum = parseInt(lineId.substring(1));
  console.assert(!isNaN(lineIdNum), "Invalid line id");

  let lineElement = document.getElementById(lineId);
  if (lineElement === null || lineElement.tagName !== "A") {
    return;
  }

  return lineIdNum;
}

// Parse and validate line range anchor in format #L${lineRangeStart}-L${lineRangeEnd}
function parseLineRangeAnchor(hashStr) {
  const hash = hashStr.substring(1).split("-");
  if (hash.length < 1 || hash.length > 2) {
    return;
  }

  let firstLine = parseLineId(hash[0]);
  let lastLine = hash.length === 2 ? parseLineId(hash[1]) : firstLine;

  if (firstLine === undefined || lastLine === undefined) {
    return;
  }

  // Swap line numbers to support "#L2-L1" format. Postel's law.
  if (firstLine > lastLine) {
    const lineTmp = lastLine;
    lastLine = firstLine;
    firstLine = lineTmp;
  }

  return [firstLine, lastLine];
}

// Highlights line number elements from firstLine to lastLine
function highlightFromTo(firstLine, lastLine) {
  const firstLineElement = document.getElementById(`L${ firstLine }`);
  const lastLineElement = document.getElementById(`L${ lastLine }`);

  const firstCodeLine = document.getElementById(`codeline-${ firstLine }`);
  const lastCodeLine = document.getElementById(`codeline-${ lastLine }`);

  addClassToRangeOfElements(firstLineElement.parentNode, lastLineElement.parentNode, "line-highlight");
  addClassToRangeOfElements(firstCodeLine, lastCodeLine, "line-highlight");
}

function clearRangeHighlight() {
  const highlightElements = Array.from(document.getElementsByClassName("line-highlight"));
  for (let el of highlightElements) {
    el.classList.remove("line-highlight");
  }
}

function addClassToRangeOfElements(first, last, class_name) {
  let element = first;
  const elementAfterLast = last !== null ? last.nextElementSibling : null;
  while (element !== null && element != elementAfterLast) {
    element.classList.add(class_name);
    element = element.nextElementSibling;
  }
}

// Sets up listeners on element that contains line numbers to handle
// shift-clicks for range highlighting
function setupLineRangeHandlers() {
  // Check if page contains the element with line numbers
  // If not, then likely script is not executed in context of the source page
  const linenodiv = document.querySelector(".linenodiv");
  if (linenodiv === null) {
    return;
  }

  let rangeStartLine, rangeEndLine;

  const parseFromHash = () => {
    const highlightedRange = parseLineRangeAnchor(window.location.hash);
    // Set range start/end to elements from hash
    if (highlightedRange !== undefined) {
      rangeStartLine = highlightedRange[0];
      rangeEndLine = highlightedRange[1];
      highlightFromTo(rangeStartLine, rangeEndLine);
      document.getElementById(`L${rangeStartLine}`).scrollIntoView();
    } else if (location.hash !== "" && location.hash[1] === "L") {
      rangeStartLine = parseLineId(location.hash.substring(1));
    }
  }

  window.addEventListener("hashchange", _ => {
    clearRangeHighlight();
    parseFromHash();
  });

  parseFromHash();

  linenodiv.addEventListener("click", ev => {
    if (ev.ctrlKey || ev.metaKey) {
      return;
    }
    ev.preventDefault();

    // Handler is set on the element that contains all line numbers, check if the
    // event is directed at an actual line number element
    const el = ev.target;
    if (typeof(el.id) !== "string" || el.id[0] !== "L" || el.tagName !== "A") {
      return;
    }

    clearRangeHighlight();

    if (rangeStartLine === undefined || !ev.shiftKey) {
      rangeStartLine = parseLineId(el.id);
      rangeEndLine = undefined;
      highlightFromTo(rangeStartLine, rangeStartLine);
      window.location.hash = el.id;
    } else if (ev.shiftKey) {
      if (rangeEndLine === undefined) {
        rangeEndLine = parseLineId(el.id);
      }

      const newLine = parseLineId(el.id);
      console.assert(newLine !== undefined, "parseLineId for clicked line is undefined");

      // Swap range elements if range end that was previously undefined is now
      // before range start
      if (rangeStartLine > rangeEndLine) {
        const lineTmp = rangeStartLine;
        rangeStartLine = rangeEndLine;
        rangeEndLine = lineTmp;
      }

      if (newLine < rangeStartLine) {
        // Expand if element above range
        rangeStartLine = newLine;
      } else if (newLine > rangeEndLine) {
        // Expand if element below range
        rangeEndLine = newLine;
      } else {
        // Shrink moving the edge that's closest to the selection.
        // Move end if center was selected.
        const distanceFromStart = Math.abs(rangeStartLine-newLine);
        const distanceFromEnd = Math.abs(rangeEndLine-newLine);
        if (distanceFromStart < distanceFromEnd) {
          rangeStartLine = newLine;
        } else {
          rangeEndLine = newLine;
        }
      }

      highlightFromTo(rangeStartLine, rangeEndLine);
      window.location.hash = `L${rangeStartLine}-L${rangeEndLine}`;
    }
  });
}

/* Other fixes */

// prevent chrome from auto-scrolling to input elements
function setupAutoscrollingPrevention() {
  const wrapper = document.querySelector('.wrapper');
  Array.prototype.forEach.call(document.querySelectorAll('input'), el => {
    el.addEventListener('keydown', _ => {
      const before = wrapper.scrollTop;
      const reset = () => wrapper.scrollTop = before;
      window.requestAnimationFrame(reset);
      setTimeout(reset, 0);
    });
  });
}

// Scrolls the page after each anchor change to prevent selected line from
// hiding under the topbar after a line number click.
function setupAnchorOffsetHandler() {
  const wrapper = document.querySelector('.wrapper');

  const anchorChangeHandler = e => {
    if (e && e.preventDefault) e.preventDefault();
    if (location.hash.length !== 0) {
      const el = document.querySelector(location.hash);
      if (el) {
        const offsetTop = el.offsetTop;
        wrapper.scrollTop = offsetTop < 100 ? 200 : offsetTop + 100;
      }
    }
  };

  window.requestAnimationFrame(anchorChangeHandler);
  window.addEventListener('hashchange', anchorChangeHandler);
}

function setupGoToTop() {
  const wrapper = document.querySelector('.wrapper');
  const goToTop = document.querySelector('.go-top');

  goToTop.addEventListener('click', e => {
    wrapper.scrollTop = 0;
    wrapper.scrollLeft = 0;
  });
}

function randomChoice(arr) {
  return arr[Math.floor(Math.random() * arr.length)];
}

function updateBannerContents(msg) {
  const banner = document.querySelector('.message-banner');

  banner.querySelector('.title').innerText = msg.title;
  banner.querySelector('.subtitle').innerHTML = msg.body.replace('\n', '<br/>');
  document.querySelector('.message-link').href = msg.link;

  const actionElement = banner.querySelector('.action');
    const actionInner = actionElement.querySelector('.action-inner');
  if (msg.action) {
    actionInner.innerText = msg.action;
    actionElement.classList.add("action-visible");
  } else {
    actionElement.classList.remove("action-visible");
  }
}

function updateMessageBanner() {
  fetch('/static/messages.json')
    .then(r => r.json())
    .then(messages => {
      // TODO compatibility with old messages format, remove after ~march 2025
      const pickedMsg = randomChoice(messages);
      const msg = pickedMsg.desktop ? pickedMsg.desktop : pickedMsg;

      updateBannerContents(msg);
    });
}

function cycleBanner(delay=500) {
  fetch('/static/messages.json')
    .then(r => r.json())
    .then(messages => {
      cycleBannerWithData(messages, delay);
    });
}

function sleep(duration) {
  return new Promise(resolve => setTimeout(resolve, duration));
}

async function cycleBannerWithData(data, delay=500) {
  for (const msg of data) {
    updateBannerContents(msg);
    await sleep(delay);
  }
  console.log("cycle finished");
}

document.addEventListener('DOMContentLoaded', _ => {
  updateMessageBanner();

  setupVersionsFilter();
  setupVersionsTree();
  setupSidebarSwitch();

  setupLineRangeHandlers();

  setupAutoscrollingPrevention();
  setupAnchorOffsetHandler();
  setupGoToTop();

  document.getElementById('clear-search').addEventListener('click',
    _ => document.getElementById('search-input').value = ''
  );
});
