'use strict';

if (!/^Win\d+/.test(navigator.platform)) {
  document.documentElement.classList.add('non-windows');
}

// polyfill for old browsers to enable [...results] and for-of
for (const type of [NodeList, NamedNodeMap, HTMLCollection, HTMLAllCollection]) {
  if (!type.prototype[Symbol.iterator]) {
    type.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
  }
}

$.remove = (selector, base = document) => {
  const el = selector && typeof selector === 'string' ? $(selector, base) : selector;
  if (el) {
    el.remove();
  }
};

$$.remove = (selector, base = document) => {
  for (const el of base.querySelectorAll(selector)) {
    el.remove();
  }
};

{
  // display a full text tooltip on buttons with ellipsis overflow and no inherent title
  const addTooltipsToEllipsized = () => {
    for (const btn of document.getElementsByTagName('button')) {
      if (btn.title && !btn.titleIsForEllipsis) {
        continue;
      }
      const width = btn.offsetWidth;
      if (!width || btn.preresizeClientWidth === width) {
        continue;
      }
      btn.preresizeClientWidth = width;
      if (btn.scrollWidth > width) {
        const text = btn.textContent;
        btn.title = text.includes('\u00AD') ? text.replace(/\u00AD/g, '') : text;
        btn.titleIsForEllipsis = true;
      } else if (btn.title) {
        btn.title = '';
      }
    }
  };
  // enqueue after DOMContentLoaded/load events
  setTimeout(addTooltipsToEllipsized, 500);
  // throttle on continuous resizing
  let timer;
  window.addEventListener('resize', () => {
    clearTimeout(timer);
    timer = setTimeout(addTooltipsToEllipsized, 100);
  });
}

onDOMready().then(() => {
  $.remove('#firefox-transitions-bug-suppressor');
  initCollapsibles();
  focusAccessibility();
});

if (!chrome.app && chrome.windows) {
  // add favicon in Firefox
  setTimeout(() => {
    if (!window.prefs) {
      return;
    }
    const iconset = ['', 'light/'][prefs.get('iconset')] || '';
    for (const size of [38, 32, 19, 16]) {
      document.head.appendChild($create('link', {
        rel: 'icon',
        href: `/images/icon/${iconset}${size}.png`,
        sizes: size + 'x' + size,
      }));
    }
  });
}

// set language for CSS :lang and [FF-only] hyphenation
document.documentElement.setAttribute('lang', chrome.i18n.getUILanguage());

function onDOMready() {
  if (document.readyState !== 'loading') {
    return Promise.resolve();
  }
  return new Promise(resolve => {
    document.addEventListener('DOMContentLoaded', function _() {
      document.removeEventListener('DOMContentLoaded', _);
      resolve();
    });
  });
}


function scrollElementIntoView(element, {invalidMarginRatio = 0} = {}) {
  // align to the top/bottom of the visible area if wasn't visible
  if (!element.parentNode) return;
  const {top, height} = element.getBoundingClientRect();
  const {top: parentTop, bottom: parentBottom} = element.parentNode.getBoundingClientRect();
  const windowHeight = window.innerHeight;
  if (top < Math.max(parentTop, windowHeight * invalidMarginRatio) ||
      top > Math.min(parentBottom, windowHeight) - height - windowHeight * invalidMarginRatio) {
    window.scrollBy(0, top - windowHeight / 2 + height);
  }
}


function animateElement(
  element, {
    className = 'highlight',
    removeExtraClasses = [],
    onComplete,
  } = {}) {
  return element && new Promise(resolve => {
    element.addEventListener('animationend', function _() {
      element.removeEventListener('animationend', _);
      element.classList.remove(
        className,
        // In Firefox, `resolve()` might be called one frame later.
        // This is helpful to clean-up on the same frame
        ...removeExtraClasses
      );
      // TODO: investigate why animation restarts for 'display' modification in .then()
      if (typeof onComplete === 'function') {
        onComplete.call(element);
      }
      resolve();
    });
    element.classList.add(className);
  });
}


function enforceInputRange(element) {
  const min = Number(element.min);
  const max = Number(element.max);
  const doNotify = () => element.dispatchEvent(new Event('change', {bubbles: true}));
  const onChange = ({type}) => {
    if (type === 'input' && element.checkValidity()) {
      doNotify();
    } else if (type === 'change' && !element.checkValidity()) {
      element.value = Math.max(min, Math.min(max, Number(element.value)));
      doNotify();
    }
  };
  element.addEventListener('change', onChange);
  element.addEventListener('input', onChange);
}


function $(selector, base = document) {
  // we have ids with . like #manage.onlyEnabled which looks like #id.class
  // so since getElementById is superfast we'll try it anyway
  const byId = selector.startsWith('#') && document.getElementById(selector.slice(1));
  return byId || base.querySelector(selector);
}


function $$(selector, base = document) {
  return [...base.querySelectorAll(selector)];
}


function $create(selector = 'div', properties, children) {
/*
  $create('tag#id.class.class', ?[children])
  $create('tag#id.class.class', ?textContentOrChildNode)
  $create('tag#id.class.class', {properties}, ?[children])
  $create('tag#id.class.class', {properties}, ?textContentOrChildNode)
           tag is 'div' by default, #id and .class are optional

  $create([children])

  $create({propertiesAndOptions})
  $create({propertiesAndOptions}, ?[children])
           tag:              string, default 'div'
           appendChild:      element/string or an array of elements/strings
           dataset:          object
           any DOM property: assigned as is

  tag may include namespace like 'ns:tag'
*/
  let ns, tag, opt;

  if (typeof selector === 'string') {
    if (Array.isArray(properties) ||
        properties instanceof Node ||
        typeof properties !== 'object') {
      opt = {};
      children = properties;
    } else {
      opt = properties || {};
      children = children || opt.appendChild;
    }
    const idStart = (selector.indexOf('#') + 1 || selector.length + 1) - 1;
    const classStart = (selector.indexOf('.') + 1 || selector.length + 1) - 1;
    const id = selector.slice(idStart + 1, classStart);
    if (id) {
      opt.id = id;
    }
    const cls = selector.slice(classStart + 1);
    if (cls) {
      opt[selector.includes(':') ? 'class' : 'className'] =
        cls.includes('.') ? cls.replace(/\./g, ' ') : cls;
    }
    tag = selector.slice(0, Math.min(idStart, classStart));

  } else if (Array.isArray(selector)) {
    tag = 'div';
    opt = {};
    children = selector;

  } else {
    opt = selector;
    tag = opt.tag;
    delete opt.tag;
    children = opt.appendChild || properties;
    delete opt.appendChild;
  }

  if (tag && tag.includes(':')) {
    ([ns, tag] = tag.split(':'));
  }

  const element = ns
    ? document.createElementNS(ns === 'SVG' || ns === 'svg' ? 'http://www.w3.org/2000/svg' : ns, tag)
    : tag === 'fragment'
      ? document.createDocumentFragment()
      : document.createElement(tag || 'div');

  for (const child of Array.isArray(children) ? children : [children]) {
    if (child) {
      element.appendChild(child instanceof Node ? child : document.createTextNode(child));
    }
  }

  if (opt.dataset) {
    Object.assign(element.dataset, opt.dataset);
    delete opt.dataset;
  }

  if (opt.attributes) {
    for (const attr in opt.attributes) {
      element.setAttribute(attr, opt.attributes[attr]);
    }
    delete opt.attributes;
  }

  if (ns) {
    for (const attr in opt) {
      const i = attr.indexOf(':') + 1;
      const attrNS = i && `http://www.w3.org/1999/${attr.slice(0, i - 1)}`;
      element.setAttributeNS(attrNS || null, attr, opt[attr]);
    }
  } else {
    Object.assign(element, opt);
  }

  return element;
}


function $createLink(href = '', content) {
  const opt = {
    tag: 'a',
    target: '_blank',
    rel: 'noopener'
  };
  if (typeof href === 'object') {
    Object.assign(opt, href);
  } else {
    opt.href = href;
  }
  opt.appendChild = opt.appendChild || content;
  return $create(opt);
}


// makes <details> with [data-pref] save/restore their state
function initCollapsibles({bindClickOn = 'h2'} = {}) {
  const prefMap = {};
  const elements = $$('details[data-pref]');
  if (!elements.length) {
    return;
  }

  for (const el of elements) {
    const key = el.dataset.pref;
    prefMap[key] = el;
    el.open = prefs.get(key);
    (bindClickOn && $(bindClickOn, el) || el).addEventListener('click', onClick);
  }

  prefs.subscribe(Object.keys(prefMap), (key, value) => {
    const el = prefMap[key];
    if (el.open !== value) {
      el.open = value;
    }
  });

  function onClick(event) {
    if (event.target.closest('.intercepts-click')) {
      event.preventDefault();
    } else {
      setTimeout(saveState, 0, event.target.closest('details'));
    }
  }

  function saveState(el) {
    prefs.set(el.dataset.pref, el.open);
  }
}

// Makes the focus outline appear on keyboard tabbing, but not on mouse clicks.
function focusAccessibility() {
  // last event's focusedViaClick
  focusAccessibility.lastFocusedViaClick = false;
  // tags of focusable elements;
  // to avoid a full layout recalc we modify the closest one
  focusAccessibility.ELEMENTS = [
    'a',
    'button',
    'input',
    'textarea',
    'label',
    'select',
    'summary',
  ];
  // try to find a focusable parent for this many parentElement jumps:
  const GIVE_UP_DEPTH = 4;

  addEventListener('mousedown', suppressOutlineOnClick, {passive: true});
  addEventListener('keydown', keepOutlineOnTab, {passive: true});

  function suppressOutlineOnClick({target}) {
    for (let el = target, i = 0; el && i++ < GIVE_UP_DEPTH; el = el.parentElement) {
      if (focusAccessibility.ELEMENTS.includes(el.localName)) {
        focusAccessibility.lastFocusedViaClick = true;
        if (el.dataset.focusedViaClick === undefined) {
          el.dataset.focusedViaClick = '';
        }
        return;
      }
    }
  }

  function keepOutlineOnTab(event) {
    if (event.which === 9) {
      focusAccessibility.lastFocusedViaClick = false;
      setTimeout(keepOutlineOnTab, 0, true);
      return;
    } else if (event !== true) {
      return;
    }
    let el = document.activeElement;
    if (!el || !focusAccessibility.ELEMENTS.includes(el.localName)) {
      return;
    }
    if (el.dataset.focusedViaClick !== undefined) {
      delete el.dataset.focusedViaClick;
    }
    el = el.closest('[data-focused-via-click]');
    if (el) {
      delete el.dataset.focusedViaClick;
    }
  }
}

/**
 * Switches to the next/previous keyboard-focusable element
 * @param {HTMLElement} rootElement
 * @param {Number} step - for exmaple 1 or -1
 * @returns {HTMLElement|false|undefined} -
 *   HTMLElement: focus changed,
 *   false: focus unchanged,
 *   undefined: nothing to focus
 */
function moveFocus(rootElement, step) {
  const elements = [...rootElement.getElementsByTagName('*')];
  const activeIndex = Math.max(0, elements.indexOf(document.activeElement));
  const num = elements.length;
  const {activeElement} = document;
  for (let i = 1; i < num; i++) {
    const elementIndex = (activeIndex + i * step + num) % num;
    // we don't use positive tabindex so we stop at any valid value
    const el = elements[elementIndex];
    if (!el.disabled && el.tabIndex >= 0) {
      el.focus();
      return activeElement !== el && el;
    }
  }
}