/*!
 * Parts of original code from ipv6.js <https://github.com/beaugunderson/javascript-ipv6>
 * Copyright 2011 Beau Gunderson
 * Available under MIT license <http://mths.be/mit>
 */

/* globals punycode:false */

const RE_V4 = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|0x[0-9a-f][0-9a-f]?|0[0-7]{3})\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|0x[0-9a-f][0-9a-f]?|0[0-7]{3})$/i;
const RE_V4_HEX = /^0x([0-9a-f]{8})$/i;
const RE_V4_NUMERIC = /^[0-9]+$/;
const RE_V4inV6 = /(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;

const RE_BAD_CHARACTERS = /([^0-9a-f:])/i;
const RE_BAD_ADDRESS = /([0-9a-f]{5,}|:{3,}|[^:]:$|^:[^:]$)/i;

function isIPv4(address) {
  if (RE_V4.test(address)) {
    return true;
  }
  if (RE_V4_HEX.test(address)) {
    return true;
  }
  if (RE_V4_NUMERIC.test(address)) {
    return true;
  }
  return false;
}

function isIPv6(address) {
  var a4addon = 0;
  var address4 = address.match(RE_V4inV6);
  if (address4) {
    var temp4 = address4[0].split('.');
    for (var i = 0; i < 4; i++) {
      if (/^0[0-9]+/.test(temp4[i])) {
        return false;
      }
    }
    address = address.replace(RE_V4inV6, '');
    if (/[0-9]$/.test(address)) {
      return false;
    }

    address = address + temp4.join(':');
    a4addon = 2;
  }

  if (RE_BAD_CHARACTERS.test(address)) {
    return false;
  }

  if (RE_BAD_ADDRESS.test(address)) {
    return false;
  }

  function count(string, substring) {
    return (string.length - string.replace(new RegExp(substring,"g"), '').length) / substring.length;
  }

  var halves = count(address, '::');
  if (halves == 1 && count(address, ':') <= 6 + 2 + a4addon) {
    return true;
  }
  if (halves == 0 && count(address, ':') == 7 + a4addon) {
    return true;
  }
  return false;
}

/**
 * Returns base domain for specified host based on Public Suffix List.
 * @param {String} hostname The name of the host to get the base domain for
 */
function getBaseDomain(/**String*/ hostname) /**String*/ {
  // remove trailing dot(s)
  hostname = hostname.replace(/\.+$/, '');

  // return IP address untouched
  if (isIPv6(hostname) || isIPv4(hostname)) {
    return hostname;
  }

  // decode punycode if exists
  if (hostname.indexOf('xn--') >= 0) {
    hostname = punycode.toUnicode(hostname);
  }

  // search through PSL
  var prevDomains = [];
  var curDomain = hostname;
  var nextDot = curDomain.indexOf('.');
  var tld = 0;

  for (;;) {
    var suffix = window.publicSuffixes[curDomain];
    if (typeof suffix != 'undefined') {
      tld = suffix;
      break;
    }

    if (nextDot < 0) {
      tld = 1;
      break;
    }

    prevDomains.push(curDomain.substring(0,nextDot));
    curDomain = curDomain.substring(nextDot+1);
    nextDot = curDomain.indexOf('.');
  }

  while (tld > 0 && prevDomains.length > 0) {
    curDomain = prevDomains.pop() + '.' + curDomain;
    tld--;
  }

  return curDomain;
}

/**
 * Converts an IP address to a number. If given input is not a valid IP address
 * then 0 is returned.
 * @param {String} ip The IP address to convert
 * @returns {Integer}
 */
function ipAddressToNumber(ip) {
  // Separate IP address into octets, make sure there are four.
  var octets = ip.split(".");
  if (octets.length !== 4) {
    return 0;
  }

  var result = 0;
  var maxOctetIndex = 3;
  for (var i = maxOctetIndex; i >= 0; i--) {
    var octet = parseInt(octets[maxOctetIndex - i], 10);

    // If octet is invalid return early, no need to continue.
    if (Number.isNaN(octet) || octet < 0 || octet > 255) {
      return 0;
    }

    // Use bit shifting to store each octet for result.
    result |= octet << (i * 8); // eslint-disable-line no-bitwise
  }

  // Results of bitwise operations in JS are interpreted as signed
  // so use zero-fill right shift to return unsigned number.
  return result >>> 0; // eslint-disable-line no-bitwise
}

/**
 * Determines if domain is private, that is localhost or the IP address spaces
 * specified by RFC 1918.
 * @param {String} domain The domain to check
 * @returns {Boolean}
 */
function isPrivateDomain(domain) { // eslint-disable-line no-unused-vars
  // Check for localhost match.
  if (domain === "localhost") {
    return true;
  }

  // Check for private IP match.
  var ipNumber = ipAddressToNumber(domain);
  var privateIpMasks = {
    "127.0.0.0": "255.0.0.0",
    "10.0.0.0": "255.0.0.0",
    "172.16.0.0": "255.240.0.0",
    "192.168.0.0": "255.255.0.0",
  };
  for (var ip in privateIpMasks) {
    // Ignore object properties.
    if (!privateIpMasks.hasOwnProperty(ip)) {
      continue;
    }

    // Compare given IP value to private IP value using bitwise AND.
    // Make sure result of AND is unsigned by using zero-fill right shift.
    var privateIpNumber = ipAddressToNumber(ip);
    var privateMaskNumber = ipAddressToNumber(privateIpMasks[ip]);
    if (((ipNumber & privateMaskNumber) >>> 0) === privateIpNumber) { // eslint-disable-line no-bitwise
      return true;
    }
  }

  // Getting here means given host didn't match localhost
  // or other private addresses so return false.
  return false;
}

/**
 * Checks whether a request is third party for the given document, uses
 * information from the public suffix list to determine the effective domain
 * name for the document.
 * @param {String} requestHost The host of the 3rd party request
 * @param {String} documentHost The host of the document
 */
function isThirdParty(/**String*/ requestHost, /**String*/ documentHost) { // eslint-disable-line no-unused-vars
  // Remove trailing dots
  requestHost = requestHost.replace(/\.+$/, "");
  documentHost = documentHost.replace(/\.+$/, "");

  // Extract domain name - leave IP addresses unchanged, otherwise leave only base domain
  var documentDomain = getBaseDomain(documentHost);
  if (requestHost.length > documentDomain.length) {
    return (requestHost.substr(requestHost.length - documentDomain.length - 1) != "." + documentDomain);
  } else {
    return (requestHost != documentDomain);
  }
}

/**
 * Extracts host name from a URL.
 */
function extractHostFromURL(/**String*/ url) { // eslint-disable-line no-unused-vars
  if (url && extractHostFromURL._lastURL == url) {
    return extractHostFromURL._lastDomain;
  }

  var host = "";
  try {
    host = new URI(url).host;
  } catch (e) {
    // Keep the empty string for invalid URIs.
  }

  extractHostFromURL._lastURL = url;
  extractHostFromURL._lastDomain = host;
  return host;
}

/**
 * Parses URLs and provides an interface similar to nsIURI in Gecko, see
 * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsIURI.
 * TODO: Make sure the parsing actually works the same as nsStandardURL.
 * @constructor
 */
function URI(/**String*/ spec) {
  this.spec = spec;
  this._schemeEnd = spec.indexOf(":");
  if (this._schemeEnd < 0) {
    throw new Error("Invalid URI scheme");
  }

  if (spec.substr(this._schemeEnd + 1, 2) != "//") {
    // special case for filesystem, blob URIs
    if (this.scheme === "filesystem" || this.scheme === "blob") {
      this._schemeEnd = spec.indexOf(":", this._schemeEnd + 1);
      if (spec.substr(this._schemeEnd + 1, 2) != "//") {
        throw new Error("Unexpected URI structure");
      }
    } else {
      throw new Error("Unexpected URI structure");
    }
  }

  this._hostPortStart = this._schemeEnd + 3;
  this._hostPortEnd = spec.indexOf("/", this._hostPortStart);
  if (this._hostPortEnd < 0) {
    throw new Error("Invalid URI host");
  }

  var authEnd = spec.indexOf("@", this._hostPortStart);
  if (authEnd >= 0 && authEnd < this._hostPortEnd) {
    this._hostPortStart = authEnd + 1;
  }

  this._portStart = -1;
  this._hostEnd = spec.indexOf("]", this._hostPortStart + 1);
  if (spec[this._hostPortStart] == "[" && this._hostEnd >= 0 && this._hostEnd < this._hostPortEnd) {
    // The host is an IPv6 literal
    this._hostStart = this._hostPortStart + 1;
    if (spec[this._hostEnd + 1] == ":") {
      this._portStart = this._hostEnd + 2;
    }
  } else {
    this._hostStart = this._hostPortStart;
    this._hostEnd = spec.indexOf(":", this._hostStart);
    if (this._hostEnd >= 0 && this._hostEnd < this._hostPortEnd) {
      this._portStart = this._hostEnd + 1;
    } else {
      this._hostEnd = this._hostPortEnd;
    }
  }
}
URI.prototype = {
  spec: null,
  get scheme() {
    return this.spec.substring(0, this._schemeEnd).toLowerCase();
  },
  get host() {
    return this.spec.substring(this._hostStart, this._hostEnd);
  },
  get asciiHost() {
    var host = this.host;
    if (/^[\x00-\x7F]+$/.test(host)) { // eslint-disable-line no-control-regex
      return host;
    } else {
      return punycode.toASCII(host);
    }
  },
  get hostPort() {
    return this.spec.substring(this._hostPortStart, this._hostPortEnd);
  },
  get port() {
    if (this._portStart < 0) {
      return -1;
    } else {
      return parseInt(this.spec.substring(this._portStart, this._hostPortEnd), 10);
    }
  },
  get path() {
    return this.spec.substring(this._hostPortEnd);
  },
  get prePath() {
    return this.spec.substring(0, this._hostPortEnd);
  }
};