/** duplicated code */
let dev = false;
let noop = () => { };
let log = {
  get error() {
    if (dev) {
      return console.error;
    }
    return noop;
  },
  get warn() {
    if (dev) {
      return console.warn;
    }
    return noop;
  },
  get info() {
    if (dev) {
      return console.log;
    }
    return noop;
  },
  get count() {
    if (dev) {
      return console.count;
    }
    return noop;
  },
}

window.addEventListener('load', async () => {
  log.info(`contentScript.js loaded`);

  let descriptors = await getDecriptiorsForUrl(window.location.href);
  descriptors = descriptors.map(d => {
    return {
      pageFragmentXPath: d.pageElement,
      nextPageButtonXPath: d.nextLink,
    }
  })
  let root = document;
  descriptorLoop:
  for (let descriptor of descriptors) {
    log.info(descriptor);

    // get append node first so we know where is "end of page"
    let appendNode = await findCommonAncestor(root, descriptor.pageFragmentXPath);
    if (!appendNode) {
      log.info('cannot find append node');
      continue descriptorLoop;
    }

    let decodeResponse = makeResponseDecodingFunction(getCurrentWebpageEncoding());
    let page = 0;
    infiniteNextPageLoop:
    while (true) {
      let nextPageButton = await getElementByXPath(root, descriptor.nextPageButtonXPath);
      if (!nextPageButton) {
        log.info('cannot find next page node');
        continue descriptorLoop;
      }

      await nodeProximityEvent(appendNode, 400);
      log.info(`reached close proximity`);

      let nextPage = await fetchNextPage(decodeResponse, nextPageButton);
      let sanitizedNextPage = await sanitizeNextPage(nextPage);
      await new Promise((resolve) => {
        setTimeout(resolve, 500);
      });
      let nextPageFragmentsXPathResult = getAllElementsByXPath(sanitizedNextPage, descriptor.pageFragmentXPath);

      page += 1;
      await append(appendNode, nextPageFragmentsXPathResult, page);

      root = nextPage;
    }
  }
});

async function getDecriptiorsForUrl(urlOfInterest) {
  log.info(`try get descriptors for url`, urlOfInterest);
  let browserInfo = await getBrowserInfo(window).catch(handleErrorAndDefault);
  let descriptors = await chrome.runtime.sendMessage({ type: "findUrlDescriptor", url: urlOfInterest, browserInfo });
  if (!descriptors || descriptors.length === 0) {
    log.info(`found no descriptors for url`, urlOfInterest);
    return [];
  }
  log.info(`found ${descriptors.length} matching descriptors for`, urlOfInterest);
  return descriptors;
}

function findCommonAncestor(root, pageFragmentXPath) {
  let appendNode = null;
  // compute first common parent for all pageFragments
  let nodes = getAllElementsByXPath(root, pageFragmentXPath);

  if (nodes.snapshotLength <= 0) {
    appendNode = null;
  } else if (nodes.snapshotLength === 1) {
    appendNode = nodes.snapshotItem(0).parentNode;
  } else if (nodes.snapshotLength >= 2) {
    let nodesArray = [];
    for (let i = 0; i < nodes.snapshotLength; i++) {
      nodesArray.push(nodes.snapshotItem(i));
    }

    appendNode = getCommonAncestor.apply(null, nodesArray);
  } else {
    throw 'whaat?!'
  }

  return appendNode;

  // https://stackoverflow.com/questions/3960843/how-to-find-the-nearest-common-ancestors-of-two-or-more-nodes/7648545#7648545
  function getCommonAncestor(node1 /*, node2, node3, ... nodeN */) {
    if (arguments.length < 2)
      throw new Error("getCommonAncestor: not enough parameters");

    let i,
      method = "contains" in node1 ? "contains" : "compareDocumentPosition",
      test = method === "contains" ? 1 : 0x0010,
      nodes = [].slice.call(arguments, 1);

    rocking:
    while (node1 = node1.parentNode) {
      i = nodes.length;
      while (i--) {
        if ((node1[method](nodes[i]) & test) !== test)
          continue rocking;
      }
      return node1;
    }

    return null;
  }
}

function getAllElementsByXPath(doc, xPath) {
  return document.evaluate(xPath, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
}

function getElementByXPath(doc, xPath) {
  return document.evaluate(xPath, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}

async function nodeProximityEvent(node, activationDistancePx) {
  return new Promise((resolve, reject) => {

    if (isEndOfPage()) {
      return resolve();
    } else {
      log.count(`subscribed to end of page event`);
      window.addEventListener("scroll", onScroll,
        {
          passive: true, useCapture: false
        }
      );
    }

    function onScroll() {
      if (isEndOfPage()) {
        window.removeEventListener("scroll", onScroll,
          {
            passive: true, useCapture: false
          }
        );
        log.count(`cancelled subscription to end of page event`);
        return resolve();
      }
    }


    return {
      cancel() {

      }
    }

    function isEndOfPage() {
      let endOfViewPortPositionY = (window.innerHeight + window.scrollY);
      let endOfNodePositionY = getOffset(node).top + node.scrollHeight;

      if (endOfNodePositionY - endOfViewPortPositionY < activationDistancePx) {
        return true;
      }
    }

    function getOffset(el) {
      const rect = el.getBoundingClientRect();
      return {
        // left: rect.left + window.scrollX,
        top: rect.top + window.scrollY
      };
    }
  })
}

async function fetchNextPage(responseDecodingFunction, nextPageElement) {
  // step 4, insert page
  let nextPageUrl = getElementHref(nextPageElement);

  let response = await fetch(nextPageUrl);
  let responseText = await responseDecodingFunction(response);
  responseText = stripHtmlTag(responseText);
  let domParser = new DOMParser();
  let nextRoot = domParser.parseFromString(responseText, 'text/html');
  return nextRoot;
}

function makeResponseDecodingFunction(pageEncoding) {
  if (!pageEncoding) {
    return defaultDecoder;
  }

  const customDecoder = maybeGetCustomDecoder(pageEncoding);
  if (!customDecoder) {
    log.warn(`encoding ${encoding} is not supported`);
    return defaultDecoder;
  }
  return customDecoder;

  async function defaultDecoder(response) {
    let responseText = await response.text();
    return responseText;
  }

  function maybeGetCustomDecoder(encoding) {
    try {
      var textDecoder = new TextDecoder(encoding);
    } catch (error) {
      // sorry
    }

    if (textDecoder) {
      return async function (response) {
        let arrayBuffer = await response.arrayBuffer();
        return textDecoder.decode(arrayBuffer);
      }
    }

    return null;
  }
}

function getCurrentWebpageEncoding() {
  return document.characterSet;
}

async function sanitizeNextPage(nextPageElement) {
  let sanitized = removeScripts(nextPageElement);

  return sanitized;
}

function append(appendNode, nextPageFragments, page) {
  appendNode.appendChild(document.createElement("hr"));
  appendNode.appendChild(document.createTextNode(`page ${page}`));
  appendNode.appendChild(document.createElement("hr"));

  for (let i = 0; i < nextPageFragments.snapshotLength; i++) {
    let pageFragment = nextPageFragments.snapshotItem(i);
    appendNode.appendChild(pageFragment);
  }
}

function removeScripts(doc) {
  let ss = doc.querySelectorAll('script')
  for (let i = 0; i < ss.length; i++) {
    ss[i].parentNode.removeChild(ss[i])
  }
  return doc
}

function getElementHref(element) {
  let href = element.getAttribute('href') || element.getAttribute('action') || element.value;
  if (href) {
    return toAbsoluteAddress(href);
  } else {
    return null;
  }
}


function toAbsoluteAddress(relativeAddress) {
  let link = document.createElement("a");
  link.href = relativeAddress;
  return link.href;
}


function stripHtmlTag(str) {
  let chunks = str.split(/(<html(?:[ \t\r\n][^>]*)?>)/)
  if (chunks.length >= 3) {
    chunks.splice(0, 2)
  }
  str = chunks.join('')
  chunks = str.split(/(<\/html[ \t\r\n]*>)/)
  if (chunks.length >= 3) {
    chunks.splice(chunks.length - 2)
  }
  return chunks.join('')
}

function createHTMLDocumentByString(str) {
  if (document.documentElement.nodeName != 'HTML') {
    return new DOMParser().parseFromString(str, 'application/xhtml+xml')
  }
  let html = strip_html_tag(str)
  let htmlDoc
  try {
    // We have to handle exceptions since Opera 9.6 throws
    // a NOT_SUPPORTED_ERR exception for |document.cloneNode(false)|
    // against the DOM 3 Core spec.
    htmlDoc = document.cloneNode(false)
    htmlDoc.appendChild(htmlDoc.importNode(document.documentElement, false))
  }
  catch (e) {
    htmlDoc = document.implementation.createDocument(null, 'html', null)
  }
  let fragment = createDocumentFragmentByString(html)
  try {
    fragment = htmlDoc.adoptNode(fragment)
  }
  catch (e) {
    fragment = htmlDoc.importNode(fragment, true)
  }
  htmlDoc.documentElement.appendChild(fragment)
  return htmlDoc
}

function createDocumentFragmentByString(str) {
  let range = document.createRange()
  range.setStartAfter(document.body)
  return range.createContextualFragment(str)
}

/** duplicated code */
async function getBrowserInfo(window) {
  let userAgentData = await getUserAgentData().catch(handleErrorAndDefault);
  let connection = await getConnectionInfo().catch(handleErrorAndDefault);
  let screen = {
    width: window.screen.width,
    height: window.screen.height,
    dpr: window.devicePixelRatio,
  }
  let viewport = {
    width: window.innerWidth,
    height: window.innerHeight,
  }
  let navigator = {
    appCodeName: window.navigator.appCodeName,
    appName: window.navigator.appName,
    appVersion: window.navigator.appVersion,
    deviceMemory: window.navigator.deviceMemory,
    doNotTrack: window.navigator.doNotTrack,
    hardwareConcurrency: window.navigator.hardwareConcurrency,
    language: window.navigator.language,
    languages: window.navigator.languages,
    maxTouchPoints: window.navigator.maxTouchPoints,
    onLine: window.navigator.onLine,
    pdfViewerEnabled: window.navigator.pdfViewerEnabled,
    platform: window.navigator.platform,
    product: window.navigator.product,
    productSub: window.navigator.productSub,
    userAgent: window.navigator.userAgent,
    vendor: window.navigator.vendor,
    vendorSub: window.navigator.vendorSub,
    webdriver: window.navigator.webdriver,
  };
  let browserInfo = {
    userAgentData,
    connection,
    screen,
    viewport,
    navigator,
  }
  return browserInfo;


  async function getUserAgentData() {
    let userAgentData = await window.navigator.userAgentData.getHighEntropyValues([
      'architecture',
      'bitness',
      'model',
      'platformVersion',
      'uaFullVersion',
      'fullVersionList',
      'wow64',
      'formFactor',
    ])
    return userAgentData;
  }

  async function getConnectionInfo() {
    return {
      downlink: window.navigator.connection.downlink,
      effectiveType: window.navigator.connection.effectiveType,
      rtt: window.navigator.connection.rtt,
      saveData: window.navigator.connection.saveData,
    };
  }

}

/** duplicated code */
function handleErrorAndDefault(error) {
  log.warn(error);
  return null;
}