/* eslint no-var: 0 */
'use strict';

var ID_PREFIX = 'stylus-';
var ROOT = document.documentElement;
var isOwnPage = location.protocol.endsWith('-extension:');
var disableAll = false;
var exposeIframes = false;
var styleElements = new Map();
var disabledElements = new Map();
var retiredStyleTimers = new Map();
var docRewriteObserver;
var docRootObserver;

requestStyles();
chrome.runtime.onMessage.addListener(applyOnMessage);

if (!isOwnPage) {
  window.dispatchEvent(new CustomEvent(chrome.runtime.id));
  window.addEventListener(chrome.runtime.id, orphanCheck, true);
}

function requestStyles(options, callback = applyStyles) {
  if (!chrome.app && document instanceof XMLDocument) {
    chrome.runtime.sendMessage({method: 'styleViaAPI', action: 'styleApply'});
    return;
  }
  var matchUrl = location.href;
  if (!matchUrl.match(/^(http|file|chrome|ftp)/)) {
    // dynamic about: and javascript: iframes don't have an URL yet
    // so we'll try the parent frame which is guaranteed to have a real URL
    try {
      if (window !== parent) {
        matchUrl = parent.location.href;
      }
    } catch (e) {}
  }
  const request = Object.assign({
    method: 'getStyles',
    matchUrl,
    enabled: true,
    asHash: true,
  }, options);
  // On own pages we request the styles directly to minimize delay and flicker
  if (typeof getStylesSafe === 'function') {
    getStylesSafe(request).then(callback);
  } else {
    chrome.runtime.sendMessage(request, callback);
  }
}


function applyOnMessage(request, sender, sendResponse) {
  if (request.styles === 'DIY') {
    // Do-It-Yourself tells our built-in pages to fetch the styles directly
    // which is faster because IPC messaging JSON-ifies everything internally
    requestStyles({}, styles => {
      request.styles = styles;
      applyOnMessage(request);
    });
    return;
  }

  if (!chrome.app && document instanceof XMLDocument && request.method !== 'ping') {
    request.action = request.method;
    request.method = 'styleViaAPI';
    request.styles = null;
    if (request.style) {
      request.style.sections = null;
    }
    chrome.runtime.sendMessage(request);
    return;
  }

  switch (request.method) {
    case 'styleDeleted':
      removeStyle(request);
      break;

    case 'styleUpdated':
      if (request.codeIsUpdated === false) {
        applyStyleState(request.style);
        break;
      }
      if (request.style.enabled) {
        removeStyle({id: request.style.id, retire: true});
        requestStyles({id: request.style.id});
      } else {
        removeStyle(request.style);
      }
      break;

    case 'styleAdded':
      if (request.style.enabled) {
        requestStyles({id: request.style.id});
      }
      break;

    case 'styleApply':
      applyStyles(request.styles);
      break;

    case 'styleReplaceAll':
      replaceAll(request.styles);
      break;

    case 'prefChanged':
      if ('disableAll' in request.prefs) {
        doDisableAll(request.prefs.disableAll);
      }
      if ('exposeIframes' in request.prefs) {
        doExposeIframes(request.prefs.exposeIframes);
      }
      break;

    case 'ping':
      sendResponse(true);
      break;
  }
}


function doDisableAll(disable = disableAll) {
  if (!disable === !disableAll) {
    return;
  }
  disableAll = disable;
  Array.prototype.forEach.call(document.styleSheets, stylesheet => {
    if (stylesheet.ownerNode.matches(`style.stylus[id^="${ID_PREFIX}"]`)
    && stylesheet.disabled !== disable) {
      stylesheet.disabled = disable;
    }
  });
}


function doExposeIframes(state = exposeIframes) {
  if (state === exposeIframes || window === parent) {
    return;
  }
  exposeIframes = state;
  const attr = document.documentElement.getAttribute('stylus-iframe');
  if (state && attr !== '') {
    document.documentElement.setAttribute('stylus-iframe', '');
  } else if (!state && attr === '') {
    document.documentElement.removeAttribute('stylus-iframe');
  }
}


function applyStyleState({id, enabled}) {
  const inCache = disabledElements.get(id) || styleElements.get(id);
  const inDoc = document.getElementById(ID_PREFIX + id);
  if (enabled) {
    if (inDoc) {
      return;
    } else if (inCache) {
      addStyleElement(inCache);
      disabledElements.delete(id);
    } else {
      requestStyles({id});
    }
  } else {
    if (inDoc) {
      disabledElements.set(id, inDoc);
      inDoc.remove();
    }
  }
}


function removeStyle({id, retire = false}) {
  const el = document.getElementById(ID_PREFIX + id);
  if (el) {
    if (retire) {
      // to avoid page flicker when the style is updated
      // instead of removing it immediately we rename its ID and queue it
      // to be deleted in applyStyles after a new version is fetched and applied
      const deadID = 'ghost-' + id;
      el.id = ID_PREFIX + deadID;
      // in case something went wrong and new style was never applied
      retiredStyleTimers.set(deadID, setTimeout(removeStyle, 1000, {id: deadID}));
    } else {
      el.remove();
    }
  }
  styleElements.delete(ID_PREFIX + id);
  disabledElements.delete(id);
  retiredStyleTimers.delete(id);
}


function applyStyles(styles) {
  if (!styles) {
    // Chrome is starting up
    requestStyles();
    return;
  }
  if ('disableAll' in styles) {
    doDisableAll(styles.disableAll);
    delete styles.disableAll;
  }
  if ('exposeIframes' in styles) {
    doExposeIframes(styles.exposeIframes);
    delete styles.exposeIframes;
  }
  if (styles.needTransitionPatch) {
    // CSS transition bug workaround: since we insert styles asynchronously,
    // the browsers, especially Firefox, may apply all transitions on page load
    delete styles.needTransitionPatch;
    const className = chrome.runtime.id + '-transition-bug-fix';
    const docId = document.documentElement.id ? '#' + document.documentElement.id : '';
    document.documentElement.classList.add(className);
    applySections(0, `
      ${docId}.${className}:root * {
        transition: none !important;
      }
    `);
    setTimeout(() => {
      removeStyle({id: 0});
      document.documentElement.classList.remove(className);
    });
  }
  for (const id in styles) {
    applySections(id, styles[id].map(section => section.code).join('\n'));
  }
  if (!isOwnPage && !docRewriteObserver && styleElements.size) {
    initDocRewriteObserver();
  }
  if (!docRootObserver && styleElements.size) {
    initDocRootObserver();
  }
  if (retiredStyleTimers.size) {
    setTimeout(() => {
      for (const [id, timer] of retiredStyleTimers.entries()) {
        removeStyle({id});
        clearTimeout(timer);
      }
    });
  }
}


function applySections(styleId, code) {
  let el = document.getElementById(ID_PREFIX + styleId);
  if (el) {
    return;
  }
  if (document.documentElement instanceof SVGSVGElement) {
    // SVG document style
    el = document.createElementNS('http://www.w3.org/2000/svg', 'style');
  } else if (document instanceof XMLDocument) {
    // XML document style
    el = document.createElementNS('http://www.w3.org/1999/xhtml', 'style');
  } else {
    // HTML document style; also works on HTML-embedded SVG
    el = document.createElement('style');
  }
  Object.assign(el, {
    id: ID_PREFIX + styleId,
    type: 'text/css',
    textContent: code,
  });
  // SVG className is not a string, but an instance of SVGAnimatedString
  el.classList.add('stylus');
  addStyleElement(el);
  styleElements.set(el.id, el);
  disabledElements.delete(Number(styleId));
  return el;
}


function addStyleElement(el) {
  if (ROOT && !document.getElementById(el.id)) {
    ROOT.appendChild(el);
    if (disableAll) {
      el.disabled = true;
    }
  }
}


function replaceAll(newStyles) {
  const oldStyles = Array.prototype.slice.call(
    document.querySelectorAll(`style.stylus[id^="${ID_PREFIX}"]`));
  oldStyles.forEach(el => (el.id += '-ghost'));
  styleElements.clear();
  disabledElements.clear();
  [...retiredStyleTimers.values()].forEach(clearTimeout);
  retiredStyleTimers.clear();
  applyStyles(newStyles);
  oldStyles.forEach(el => el.remove());
}


function initDocRewriteObserver() {
  // re-add styles if we detect documentElement being recreated
  const reinjectStyles = () => {
    if (!styleElements) {
      return orphanCheck && orphanCheck();
    }
    ROOT = document.documentElement;
    for (const el of styleElements.values()) {
      el.textContent += ' '; // invalidate CSSOM cache
      addStyleElement(document.importNode(el, true));
    }
  };
  // detect documentElement being rewritten from inside the script
  docRewriteObserver = new MutationObserver(mutations => {
    for (let m = mutations.length; --m >= 0;) {
      const added = mutations[m].addedNodes;
      for (let n = added.length; --n >= 0;) {
        if (added[n].localName === 'html') {
          reinjectStyles();
          return;
        }
      }
    }
  });
  docRewriteObserver.observe(document, {childList: true});
  // detect dynamic iframes rewritten after creation by the embedder i.e. externally
  setTimeout(() => {
    if (document.documentElement !== ROOT) {
      reinjectStyles();
    }
  });
}


function initDocRootObserver() {
  let lastRestorationTime = 0;
  let restorationCounter = 0;

  docRootObserver = new MutationObserver(findMisplacedStyles);
  connectObserver();

  function connectObserver() {
    docRootObserver.observe(ROOT, {childList: true});
  }

  function findMisplacedStyles() {
    let expectedPrevSibling = document.body || document.head;
    if (!expectedPrevSibling) {
      return;
    }
    const list = [];
    for (const [id, el] of styleElements.entries()) {
      if (!disabledElements.has(parseInt(id.substr(ID_PREFIX.length))) &&
          el.previousElementSibling !== expectedPrevSibling) {
        list.push({el, before: expectedPrevSibling.nextSibling});
      }
      expectedPrevSibling = el;
    }
    if (list.length && !restorationLimitExceeded()) {
      restoreMisplacedStyles(list);
    }
  }

  function restoreMisplacedStyles(list) {
    docRootObserver.disconnect();
    for (const {el, before} of list) {
      ROOT.insertBefore(el, before);
      if (el.disabled !== disableAll) {
        // moving an element resets its 'disabled' state
        el.disabled = disableAll;
      }
    }
    connectObserver();
  }

  function restorationLimitExceeded() {
    const t = performance.now();
    if (t - lastRestorationTime > 1000) {
      restorationCounter = 0;
    }
    lastRestorationTime = t;
    if (++restorationCounter > 100) {
      console.error('Stylus stopped restoring userstyle elements after 100 failed attempts.\n' +
        'Please report on https://github.com/openstyles/stylus/issues');
      return true;
    }
  }
}


function orphanCheck() {
  const port = chrome.runtime.connect();
  if (port) {
    port.disconnect();
    return;
  }

  // we're orphaned due to an extension update
  // we can detach the mutation observer
  [docRewriteObserver, docRootObserver].forEach(ob => ob && ob.disconnect());
  // we can detach event listeners
  window.removeEventListener(chrome.runtime.id, orphanCheck, true);
  // we can't detach chrome.runtime.onMessage because it's no longer connected internally
  // we can destroy our globals in this context to free up memory
  [ // functions
    'addStyleElement',
    'applyOnMessage',
    'applySections',
    'applyStyles',
    'applyStyleState',
    'doDisableAll',
    'initDocRewriteObserver',
    'initDocRootObserver',
    'orphanCheck',
    'removeStyle',
    'replaceAll',
    'requestStyles',
    // variables
    'ROOT',
    'disabledElements',
    'retiredStyleTimers',
    'styleElements',
    'docRewriteObserver',
    'docRootObserver',
  ].forEach(fn => (window[fn] = null));
}