'use strict';
const punycode = require('punycode');
const tr46 = require('tr46');

const specialSchemes = {
  ftp: 21,
  file: null,
  gopher: 70,
  http: 80,
  https: 443,
  ws: 80,
  wss: 443,
};

const failure = Symbol('failure');

function countSymbols(str) {
  return punycode.ucs2.decode(str).length;
}

function at(input, idx) {
  const c = input[idx];
  return isNaN(c) ? undefined : String.fromCodePoint(c);
}

function isASCIIDigit(c) {
  return c >= 0x30 && c <= 0x39;
}

function isASCIIAlpha(c) {
  return (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a);
}

function isASCIIAlphanumeric(c) {
  return isASCIIAlpha(c) || isASCIIDigit(c);
}

function isASCIIHex(c) {
  return (
    isASCIIDigit(c) || (c >= 0x41 && c <= 0x46) || (c >= 0x61 && c <= 0x66)
  );
}

function isSingleDot(buffer) {
  return buffer === '.' || buffer.toLowerCase() === '%2e';
}

function isDoubleDot(buffer) {
  buffer = buffer.toLowerCase();
  return (
    buffer === '..' ||
    buffer === '%2e.' ||
    buffer === '.%2e' ||
    buffer === '%2e%2e'
  );
}

function isWindowsDriveLetterCodePoints(cp1, cp2) {
  return isASCIIAlpha(cp1) && (cp2 === 58 || cp2 === 124);
}

function isWindowsDriveLetterString(string) {
  return (
    string.length === 2 &&
    isASCIIAlpha(string.codePointAt(0)) &&
    (string[1] === ':' || string[1] === '|')
  );
}

function isNormalizedWindowsDriveLetterString(string) {
  return (
    string.length === 2 &&
    isASCIIAlpha(string.codePointAt(0)) &&
    string[1] === ':'
  );
}

function containsForbiddenHostCodePoint(string) {
  return (
    string.search(
      /\u0000|\u0009|\u000A|\u000D|\u0020|#|%|\/|:|\?|@|\[|\\|\]/
    ) !== -1
  );
}

function containsForbiddenHostCodePointExcludingPercent(string) {
  return (
    string.search(/\u0000|\u0009|\u000A|\u000D|\u0020|#|\/|:|\?|@|\[|\\|\]/) !==
    -1
  );
}

function isSpecialScheme(scheme) {
  return specialSchemes[scheme] !== undefined;
}

function isSpecial(url) {
  return isSpecialScheme(url.scheme);
}

function defaultPort(scheme) {
  return specialSchemes[scheme];
}

function percentEncode(c) {
  let hex = c.toString(16).toUpperCase();
  if (hex.length === 1) {
    hex = '0' + hex;
  }

  return '%' + hex;
}

function utf8PercentEncode(c) {
  const buf = new Buffer(c);

  let str = '';

  for (let i = 0; i < buf.length; ++i) {
    str += percentEncode(buf[i]);
  }

  return str;
}

function utf8PercentDecode(str) {
  const input = new Buffer(str);
  const output = [];
  for (let i = 0; i < input.length; ++i) {
    if (input[i] !== 37) {
      output.push(input[i]);
    } else if (
      input[i] === 37 &&
      isASCIIHex(input[i + 1]) &&
      isASCIIHex(input[i + 2])
    ) {
      output.push(parseInt(input.slice(i + 1, i + 3).toString(), 16));
      i += 2;
    } else {
      output.push(input[i]);
    }
  }
  return new Buffer(output).toString();
}

function isC0ControlPercentEncode(c) {
  return c <= 0x1f || c > 0x7e;
}

const extraPathPercentEncodeSet = new Set([
  32, 34, 35, 60, 62, 63, 96, 123, 125,
]);
function isPathPercentEncode(c) {
  return isC0ControlPercentEncode(c) || extraPathPercentEncodeSet.has(c);
}

const extraUserinfoPercentEncodeSet = new Set([
  47, 58, 59, 61, 64, 91, 92, 93, 94, 124,
]);
function isUserinfoPercentEncode(c) {
  return isPathPercentEncode(c) || extraUserinfoPercentEncodeSet.has(c);
}

function percentEncodeChar(c, encodeSetPredicate) {
  const cStr = String.fromCodePoint(c);

  if (encodeSetPredicate(c)) {
    return utf8PercentEncode(cStr);
  }

  return cStr;
}

function parseIPv4Number(input) {
  let R = 10;

  if (
    input.length >= 2 &&
    input.charAt(0) === '0' &&
    input.charAt(1).toLowerCase() === 'x'
  ) {
    input = input.substring(2);
    R = 16;
  } else if (input.length >= 2 && input.charAt(0) === '0') {
    input = input.substring(1);
    R = 8;
  }

  if (input === '') {
    return 0;
  }

  const regex =
    R === 10 ? /[^0-9]/
    : R === 16 ? /[^0-9A-Fa-f]/
    : /[^0-7]/;
  if (regex.test(input)) {
    return failure;
  }

  return parseInt(input, R);
}

function parseIPv4(input) {
  const parts = input.split('.');
  if (parts[parts.length - 1] === '') {
    if (parts.length > 1) {
      parts.pop();
    }
  }

  if (parts.length > 4) {
    return input;
  }

  const numbers = [];
  for (const part of parts) {
    if (part === '') {
      return input;
    }
    const n = parseIPv4Number(part);
    if (n === failure) {
      return input;
    }

    numbers.push(n);
  }

  for (let i = 0; i < numbers.length - 1; ++i) {
    if (numbers[i] > 255) {
      return failure;
    }
  }
  if (numbers[numbers.length - 1] >= Math.pow(256, 5 - numbers.length)) {
    return failure;
  }

  let ipv4 = numbers.pop();
  let counter = 0;

  for (const n of numbers) {
    ipv4 += n * Math.pow(256, 3 - counter);
    ++counter;
  }

  return ipv4;
}

function serializeIPv4(address) {
  let output = '';
  let n = address;

  for (let i = 1; i <= 4; ++i) {
    output = String(n % 256) + output;
    if (i !== 4) {
      output = '.' + output;
    }
    n = Math.floor(n / 256);
  }

  return output;
}

function parseIPv6(input) {
  const address = [0, 0, 0, 0, 0, 0, 0, 0];
  let pieceIndex = 0;
  let compress = null;
  let pointer = 0;

  input = punycode.ucs2.decode(input);

  if (input[pointer] === 58) {
    if (input[pointer + 1] !== 58) {
      return failure;
    }

    pointer += 2;
    ++pieceIndex;
    compress = pieceIndex;
  }

  while (pointer < input.length) {
    if (pieceIndex === 8) {
      return failure;
    }

    if (input[pointer] === 58) {
      if (compress !== null) {
        return failure;
      }
      ++pointer;
      ++pieceIndex;
      compress = pieceIndex;
      continue;
    }

    let value = 0;
    let length = 0;

    while (length < 4 && isASCIIHex(input[pointer])) {
      value = value * 0x10 + parseInt(at(input, pointer), 16);
      ++pointer;
      ++length;
    }

    if (input[pointer] === 46) {
      if (length === 0) {
        return failure;
      }

      pointer -= length;

      if (pieceIndex > 6) {
        return failure;
      }

      let numbersSeen = 0;

      while (input[pointer] !== undefined) {
        let ipv4Piece = null;

        if (numbersSeen > 0) {
          if (input[pointer] === 46 && numbersSeen < 4) {
            ++pointer;
          } else {
            return failure;
          }
        }

        if (!isASCIIDigit(input[pointer])) {
          return failure;
        }

        while (isASCIIDigit(input[pointer])) {
          const number = parseInt(at(input, pointer));
          if (ipv4Piece === null) {
            ipv4Piece = number;
          } else if (ipv4Piece === 0) {
            return failure;
          } else {
            ipv4Piece = ipv4Piece * 10 + number;
          }
          if (ipv4Piece > 255) {
            return failure;
          }
          ++pointer;
        }

        address[pieceIndex] = address[pieceIndex] * 0x100 + ipv4Piece;

        ++numbersSeen;

        if (numbersSeen === 2 || numbersSeen === 4) {
          ++pieceIndex;
        }
      }

      if (numbersSeen !== 4) {
        return failure;
      }

      break;
    } else if (input[pointer] === 58) {
      ++pointer;
      if (input[pointer] === undefined) {
        return failure;
      }
    } else if (input[pointer] !== undefined) {
      return failure;
    }

    address[pieceIndex] = value;
    ++pieceIndex;
  }

  if (compress !== null) {
    let swaps = pieceIndex - compress;
    pieceIndex = 7;
    while (pieceIndex !== 0 && swaps > 0) {
      const temp = address[compress + swaps - 1];
      address[compress + swaps - 1] = address[pieceIndex];
      address[pieceIndex] = temp;
      --pieceIndex;
      --swaps;
    }
  } else if (compress === null && pieceIndex !== 8) {
    return failure;
  }

  return address;
}

function serializeIPv6(address) {
  let output = '';
  const seqResult = findLongestZeroSequence(address);
  const compress = seqResult.idx;
  let ignore0 = false;

  for (let pieceIndex = 0; pieceIndex <= 7; ++pieceIndex) {
    if (ignore0 && address[pieceIndex] === 0) {
      continue;
    } else if (ignore0) {
      ignore0 = false;
    }

    if (compress === pieceIndex) {
      const separator = pieceIndex === 0 ? '::' : ':';
      output += separator;
      ignore0 = true;
      continue;
    }

    output += address[pieceIndex].toString(16);

    if (pieceIndex !== 7) {
      output += ':';
    }
  }

  return output;
}

function parseHost(input, isSpecialArg) {
  if (input[0] === '[') {
    if (input[input.length - 1] !== ']') {
      return failure;
    }

    return parseIPv6(input.substring(1, input.length - 1));
  }

  if (!isSpecialArg) {
    return parseOpaqueHost(input);
  }

  const domain = utf8PercentDecode(input);
  const asciiDomain = tr46.toASCII(
    domain,
    false,
    tr46.PROCESSING_OPTIONS.NONTRANSITIONAL,
    false
  );
  if (asciiDomain === null) {
    return failure;
  }

  if (containsForbiddenHostCodePoint(asciiDomain)) {
    return failure;
  }

  const ipv4Host = parseIPv4(asciiDomain);
  if (typeof ipv4Host === 'number' || ipv4Host === failure) {
    return ipv4Host;
  }

  return asciiDomain;
}

function parseOpaqueHost(input) {
  if (containsForbiddenHostCodePointExcludingPercent(input)) {
    return failure;
  }

  let output = '';
  const decoded = punycode.ucs2.decode(input);
  for (let i = 0; i < decoded.length; ++i) {
    output += percentEncodeChar(decoded[i], isC0ControlPercentEncode);
  }
  return output;
}

function findLongestZeroSequence(arr) {
  let maxIdx = null;
  let maxLen = 1; // only find elements > 1
  let currStart = null;
  let currLen = 0;

  for (let i = 0; i < arr.length; ++i) {
    if (arr[i] !== 0) {
      if (currLen > maxLen) {
        maxIdx = currStart;
        maxLen = currLen;
      }

      currStart = null;
      currLen = 0;
    } else {
      if (currStart === null) {
        currStart = i;
      }
      ++currLen;
    }
  }

  // if trailing zeros
  if (currLen > maxLen) {
    maxIdx = currStart;
    maxLen = currLen;
  }

  return {
    idx: maxIdx,
    len: maxLen,
  };
}

function serializeHost(host) {
  if (typeof host === 'number') {
    return serializeIPv4(host);
  }

  // IPv6 serializer
  if (host instanceof Array) {
    return '[' + serializeIPv6(host) + ']';
  }

  return host;
}

function trimControlChars(url) {
  return url.replace(/^[\u0000-\u001F\u0020]+|[\u0000-\u001F\u0020]+$/g, '');
}

function trimTabAndNewline(url) {
  return url.replace(/\u0009|\u000A|\u000D/g, '');
}

function shortenPath(url) {
  const path = url.path;
  if (path.length === 0) {
    return;
  }
  if (
    url.scheme === 'file' &&
    path.length === 1 &&
    isNormalizedWindowsDriveLetter(path[0])
  ) {
    return;
  }

  path.pop();
}

function includesCredentials(url) {
  return url.username !== '' || url.password !== '';
}

function cannotHaveAUsernamePasswordPort(url) {
  return (
    url.host === null ||
    url.host === '' ||
    url.cannotBeABaseURL ||
    url.scheme === 'file'
  );
}

function isNormalizedWindowsDriveLetter(string) {
  return /^[A-Za-z]:$/.test(string);
}

function URLStateMachine(input, base, encodingOverride, url, stateOverride) {
  this.pointer = 0;
  this.input = input;
  this.base = base || null;
  this.encodingOverride = encodingOverride || 'utf-8';
  this.stateOverride = stateOverride;
  this.url = url;
  this.failure = false;
  this.parseError = false;

  if (!this.url) {
    this.url = {
      scheme: '',
      username: '',
      password: '',
      host: null,
      port: null,
      path: [],
      query: null,
      fragment: null,

      cannotBeABaseURL: false,
    };

    const res = trimControlChars(this.input);
    if (res !== this.input) {
      this.parseError = true;
    }
    this.input = res;
  }

  const res = trimTabAndNewline(this.input);
  if (res !== this.input) {
    this.parseError = true;
  }
  this.input = res;

  this.state = stateOverride || 'scheme start';

  this.buffer = '';
  this.atFlag = false;
  this.arrFlag = false;
  this.passwordTokenSeenFlag = false;

  this.input = punycode.ucs2.decode(this.input);

  for (; this.pointer <= this.input.length; ++this.pointer) {
    const c = this.input[this.pointer];
    const cStr = isNaN(c) ? undefined : String.fromCodePoint(c);

    // exec state machine
    const ret = this['parse ' + this.state](c, cStr);
    if (!ret) {
      break; // terminate algorithm
    } else if (ret === failure) {
      this.failure = true;
      break;
    }
  }
}

URLStateMachine.prototype['parse scheme start'] = function parseSchemeStart(
  c,
  cStr
) {
  if (isASCIIAlpha(c)) {
    this.buffer += cStr.toLowerCase();
    this.state = 'scheme';
  } else if (!this.stateOverride) {
    this.state = 'no scheme';
    --this.pointer;
  } else {
    this.parseError = true;
    return failure;
  }

  return true;
};

URLStateMachine.prototype['parse scheme'] = function parseScheme(c, cStr) {
  if (isASCIIAlphanumeric(c) || c === 43 || c === 45 || c === 46) {
    this.buffer += cStr.toLowerCase();
  } else if (c === 58) {
    if (this.stateOverride) {
      if (isSpecial(this.url) && !isSpecialScheme(this.buffer)) {
        return false;
      }

      if (!isSpecial(this.url) && isSpecialScheme(this.buffer)) {
        return false;
      }

      if (
        (includesCredentials(this.url) || this.url.port !== null) &&
        this.buffer === 'file'
      ) {
        return false;
      }

      if (
        this.url.scheme === 'file' &&
        (this.url.host === '' || this.url.host === null)
      ) {
        return false;
      }
    }
    this.url.scheme = this.buffer;
    this.buffer = '';
    if (this.stateOverride) {
      return false;
    }
    if (this.url.scheme === 'file') {
      if (
        this.input[this.pointer + 1] !== 47 ||
        this.input[this.pointer + 2] !== 47
      ) {
        this.parseError = true;
      }
      this.state = 'file';
    } else if (
      isSpecial(this.url) &&
      this.base !== null &&
      this.base.scheme === this.url.scheme
    ) {
      this.state = 'special relative or authority';
    } else if (isSpecial(this.url)) {
      this.state = 'special authority slashes';
    } else if (this.input[this.pointer + 1] === 47) {
      this.state = 'path or authority';
      ++this.pointer;
    } else {
      this.url.cannotBeABaseURL = true;
      this.url.path.push('');
      this.state = 'cannot-be-a-base-URL path';
    }
  } else if (!this.stateOverride) {
    this.buffer = '';
    this.state = 'no scheme';
    this.pointer = -1;
  } else {
    this.parseError = true;
    return failure;
  }

  return true;
};

URLStateMachine.prototype['parse no scheme'] = function parseNoScheme(c) {
  if (this.base === null || (this.base.cannotBeABaseURL && c !== 35)) {
    return failure;
  } else if (this.base.cannotBeABaseURL && c === 35) {
    this.url.scheme = this.base.scheme;
    this.url.path = this.base.path.slice();
    this.url.query = this.base.query;
    this.url.fragment = '';
    this.url.cannotBeABaseURL = true;
    this.state = 'fragment';
  } else if (this.base.scheme === 'file') {
    this.state = 'file';
    --this.pointer;
  } else {
    this.state = 'relative';
    --this.pointer;
  }

  return true;
};

URLStateMachine.prototype['parse special relative or authority'] =
  function parseSpecialRelativeOrAuthority(c) {
    if (c === 47 && this.input[this.pointer + 1] === 47) {
      this.state = 'special authority ignore slashes';
      ++this.pointer;
    } else {
      this.parseError = true;
      this.state = 'relative';
      --this.pointer;
    }

    return true;
  };

URLStateMachine.prototype['parse path or authority'] =
  function parsePathOrAuthority(c) {
    if (c === 47) {
      this.state = 'authority';
    } else {
      this.state = 'path';
      --this.pointer;
    }

    return true;
  };

URLStateMachine.prototype['parse relative'] = function parseRelative(c) {
  this.url.scheme = this.base.scheme;
  if (isNaN(c)) {
    this.url.username = this.base.username;
    this.url.password = this.base.password;
    this.url.host = this.base.host;
    this.url.port = this.base.port;
    this.url.path = this.base.path.slice();
    this.url.query = this.base.query;
  } else if (c === 47) {
    this.state = 'relative slash';
  } else if (c === 63) {
    this.url.username = this.base.username;
    this.url.password = this.base.password;
    this.url.host = this.base.host;
    this.url.port = this.base.port;
    this.url.path = this.base.path.slice();
    this.url.query = '';
    this.state = 'query';
  } else if (c === 35) {
    this.url.username = this.base.username;
    this.url.password = this.base.password;
    this.url.host = this.base.host;
    this.url.port = this.base.port;
    this.url.path = this.base.path.slice();
    this.url.query = this.base.query;
    this.url.fragment = '';
    this.state = 'fragment';
  } else if (isSpecial(this.url) && c === 92) {
    this.parseError = true;
    this.state = 'relative slash';
  } else {
    this.url.username = this.base.username;
    this.url.password = this.base.password;
    this.url.host = this.base.host;
    this.url.port = this.base.port;
    this.url.path = this.base.path.slice(0, this.base.path.length - 1);

    this.state = 'path';
    --this.pointer;
  }

  return true;
};

URLStateMachine.prototype['parse relative slash'] = function parseRelativeSlash(
  c
) {
  if (isSpecial(this.url) && (c === 47 || c === 92)) {
    if (c === 92) {
      this.parseError = true;
    }
    this.state = 'special authority ignore slashes';
  } else if (c === 47) {
    this.state = 'authority';
  } else {
    this.url.username = this.base.username;
    this.url.password = this.base.password;
    this.url.host = this.base.host;
    this.url.port = this.base.port;
    this.state = 'path';
    --this.pointer;
  }

  return true;
};

URLStateMachine.prototype['parse special authority slashes'] =
  function parseSpecialAuthoritySlashes(c) {
    if (c === 47 && this.input[this.pointer + 1] === 47) {
      this.state = 'special authority ignore slashes';
      ++this.pointer;
    } else {
      this.parseError = true;
      this.state = 'special authority ignore slashes';
      --this.pointer;
    }

    return true;
  };

URLStateMachine.prototype['parse special authority ignore slashes'] =
  function parseSpecialAuthorityIgnoreSlashes(c) {
    if (c !== 47 && c !== 92) {
      this.state = 'authority';
      --this.pointer;
    } else {
      this.parseError = true;
    }

    return true;
  };

URLStateMachine.prototype['parse authority'] = function parseAuthority(
  c,
  cStr
) {
  if (c === 64) {
    this.parseError = true;
    if (this.atFlag) {
      this.buffer = '%40' + this.buffer;
    }
    this.atFlag = true;

    // careful, this is based on buffer and has its own pointer (this.pointer != pointer) and inner chars
    const len = countSymbols(this.buffer);
    for (let pointer = 0; pointer < len; ++pointer) {
      const codePoint = this.buffer.codePointAt(pointer);

      if (codePoint === 58 && !this.passwordTokenSeenFlag) {
        this.passwordTokenSeenFlag = true;
        continue;
      }
      const encodedCodePoints = percentEncodeChar(
        codePoint,
        isUserinfoPercentEncode
      );
      if (this.passwordTokenSeenFlag) {
        this.url.password += encodedCodePoints;
      } else {
        this.url.username += encodedCodePoints;
      }
    }
    this.buffer = '';
  } else if (
    isNaN(c) ||
    c === 47 ||
    c === 63 ||
    c === 35 ||
    (isSpecial(this.url) && c === 92)
  ) {
    if (this.atFlag && this.buffer === '') {
      this.parseError = true;
      return failure;
    }
    this.pointer -= countSymbols(this.buffer) + 1;
    this.buffer = '';
    this.state = 'host';
  } else {
    this.buffer += cStr;
  }

  return true;
};

URLStateMachine.prototype['parse hostname'] = URLStateMachine.prototype[
  'parse host'
] = function parseHostName(c, cStr) {
  if (this.stateOverride && this.url.scheme === 'file') {
    --this.pointer;
    this.state = 'file host';
  } else if (c === 58 && !this.arrFlag) {
    if (this.buffer === '') {
      this.parseError = true;
      return failure;
    }

    const host = parseHost(this.buffer, isSpecial(this.url));
    if (host === failure) {
      return failure;
    }

    this.url.host = host;
    this.buffer = '';
    this.state = 'port';
    if (this.stateOverride === 'hostname') {
      return false;
    }
  } else if (
    isNaN(c) ||
    c === 47 ||
    c === 63 ||
    c === 35 ||
    (isSpecial(this.url) && c === 92)
  ) {
    --this.pointer;
    if (isSpecial(this.url) && this.buffer === '') {
      this.parseError = true;
      return failure;
    } else if (
      this.stateOverride &&
      this.buffer === '' &&
      (includesCredentials(this.url) || this.url.port !== null)
    ) {
      this.parseError = true;
      return false;
    }

    const host = parseHost(this.buffer, isSpecial(this.url));
    if (host === failure) {
      return failure;
    }

    this.url.host = host;
    this.buffer = '';
    this.state = 'path start';
    if (this.stateOverride) {
      return false;
    }
  } else {
    if (c === 91) {
      this.arrFlag = true;
    } else if (c === 93) {
      this.arrFlag = false;
    }
    this.buffer += cStr;
  }

  return true;
};

URLStateMachine.prototype['parse port'] = function parsePort(c, cStr) {
  if (isASCIIDigit(c)) {
    this.buffer += cStr;
  } else if (
    isNaN(c) ||
    c === 47 ||
    c === 63 ||
    c === 35 ||
    (isSpecial(this.url) && c === 92) ||
    this.stateOverride
  ) {
    if (this.buffer !== '') {
      const port = parseInt(this.buffer);
      if (port > Math.pow(2, 16) - 1) {
        this.parseError = true;
        return failure;
      }
      this.url.port = port === defaultPort(this.url.scheme) ? null : port;
      this.buffer = '';
    }
    if (this.stateOverride) {
      return false;
    }
    this.state = 'path start';
    --this.pointer;
  } else {
    this.parseError = true;
    return failure;
  }

  return true;
};

const fileOtherwiseCodePoints = new Set([47, 92, 63, 35]);

URLStateMachine.prototype['parse file'] = function parseFile(c) {
  this.url.scheme = 'file';

  if (c === 47 || c === 92) {
    if (c === 92) {
      this.parseError = true;
    }
    this.state = 'file slash';
  } else if (this.base !== null && this.base.scheme === 'file') {
    if (isNaN(c)) {
      this.url.host = this.base.host;
      this.url.path = this.base.path.slice();
      this.url.query = this.base.query;
    } else if (c === 63) {
      this.url.host = this.base.host;
      this.url.path = this.base.path.slice();
      this.url.query = '';
      this.state = 'query';
    } else if (c === 35) {
      this.url.host = this.base.host;
      this.url.path = this.base.path.slice();
      this.url.query = this.base.query;
      this.url.fragment = '';
      this.state = 'fragment';
    } else {
      if (
        this.input.length - this.pointer - 1 === 0 || // remaining consists of 0 code points
        !isWindowsDriveLetterCodePoints(c, this.input[this.pointer + 1]) ||
        (this.input.length - this.pointer - 1 >= 2 && // remaining has at least 2 code points
          !fileOtherwiseCodePoints.has(this.input[this.pointer + 2]))
      ) {
        this.url.host = this.base.host;
        this.url.path = this.base.path.slice();
        shortenPath(this.url);
      } else {
        this.parseError = true;
      }

      this.state = 'path';
      --this.pointer;
    }
  } else {
    this.state = 'path';
    --this.pointer;
  }

  return true;
};

URLStateMachine.prototype['parse file slash'] = function parseFileSlash(c) {
  if (c === 47 || c === 92) {
    if (c === 92) {
      this.parseError = true;
    }
    this.state = 'file host';
  } else {
    if (this.base !== null && this.base.scheme === 'file') {
      if (isNormalizedWindowsDriveLetterString(this.base.path[0])) {
        this.url.path.push(this.base.path[0]);
      } else {
        this.url.host = this.base.host;
      }
    }
    this.state = 'path';
    --this.pointer;
  }

  return true;
};

URLStateMachine.prototype['parse file host'] = function parseFileHost(c, cStr) {
  if (isNaN(c) || c === 47 || c === 92 || c === 63 || c === 35) {
    --this.pointer;
    if (!this.stateOverride && isWindowsDriveLetterString(this.buffer)) {
      this.parseError = true;
      this.state = 'path';
    } else if (this.buffer === '') {
      this.url.host = '';
      if (this.stateOverride) {
        return false;
      }
      this.state = 'path start';
    } else {
      let host = parseHost(this.buffer, isSpecial(this.url));
      if (host === failure) {
        return failure;
      }
      if (host === 'localhost') {
        host = '';
      }
      this.url.host = host;

      if (this.stateOverride) {
        return false;
      }

      this.buffer = '';
      this.state = 'path start';
    }
  } else {
    this.buffer += cStr;
  }

  return true;
};

URLStateMachine.prototype['parse path start'] = function parsePathStart(c) {
  if (isSpecial(this.url)) {
    if (c === 92) {
      this.parseError = true;
    }
    this.state = 'path';

    if (c !== 47 && c !== 92) {
      --this.pointer;
    }
  } else if (!this.stateOverride && c === 63) {
    this.url.query = '';
    this.state = 'query';
  } else if (!this.stateOverride && c === 35) {
    this.url.fragment = '';
    this.state = 'fragment';
  } else if (c !== undefined) {
    this.state = 'path';
    if (c !== 47) {
      --this.pointer;
    }
  }

  return true;
};

URLStateMachine.prototype['parse path'] = function parsePath(c) {
  if (
    isNaN(c) ||
    c === 47 ||
    (isSpecial(this.url) && c === 92) ||
    (!this.stateOverride && (c === 63 || c === 35))
  ) {
    if (isSpecial(this.url) && c === 92) {
      this.parseError = true;
    }

    if (isDoubleDot(this.buffer)) {
      shortenPath(this.url);
      if (c !== 47 && !(isSpecial(this.url) && c === 92)) {
        this.url.path.push('');
      }
    } else if (
      isSingleDot(this.buffer) &&
      c !== 47 &&
      !(isSpecial(this.url) && c === 92)
    ) {
      this.url.path.push('');
    } else if (!isSingleDot(this.buffer)) {
      if (
        this.url.scheme === 'file' &&
        this.url.path.length === 0 &&
        isWindowsDriveLetterString(this.buffer)
      ) {
        if (this.url.host !== '' && this.url.host !== null) {
          this.parseError = true;
          this.url.host = '';
        }
        this.buffer = this.buffer[0] + ':';
      }
      this.url.path.push(this.buffer);
    }
    this.buffer = '';
    if (
      this.url.scheme === 'file' &&
      (c === undefined || c === 63 || c === 35)
    ) {
      while (this.url.path.length > 1 && this.url.path[0] === '') {
        this.parseError = true;
        this.url.path.shift();
      }
    }
    if (c === 63) {
      this.url.query = '';
      this.state = 'query';
    }
    if (c === 35) {
      this.url.fragment = '';
      this.state = 'fragment';
    }
  } else {
    // TODO: If c is not a URL code point and not "%", parse error.

    if (
      c === 37 &&
      (!isASCIIHex(this.input[this.pointer + 1]) ||
        !isASCIIHex(this.input[this.pointer + 2]))
    ) {
      this.parseError = true;
    }

    this.buffer += percentEncodeChar(c, isPathPercentEncode);
  }

  return true;
};

URLStateMachine.prototype['parse cannot-be-a-base-URL path'] =
  function parseCannotBeABaseURLPath(c) {
    if (c === 63) {
      this.url.query = '';
      this.state = 'query';
    } else if (c === 35) {
      this.url.fragment = '';
      this.state = 'fragment';
    } else {
      // TODO: Add: not a URL code point
      if (!isNaN(c) && c !== 37) {
        this.parseError = true;
      }

      if (
        c === 37 &&
        (!isASCIIHex(this.input[this.pointer + 1]) ||
          !isASCIIHex(this.input[this.pointer + 2]))
      ) {
        this.parseError = true;
      }

      if (!isNaN(c)) {
        this.url.path[0] =
          this.url.path[0] + percentEncodeChar(c, isC0ControlPercentEncode);
      }
    }

    return true;
  };

URLStateMachine.prototype['parse query'] = function parseQuery(c, cStr) {
  if (isNaN(c) || (!this.stateOverride && c === 35)) {
    if (
      !isSpecial(this.url) ||
      this.url.scheme === 'ws' ||
      this.url.scheme === 'wss'
    ) {
      this.encodingOverride = 'utf-8';
    }

    const buffer = new Buffer(this.buffer); // TODO: Use encoding override instead
    for (let i = 0; i < buffer.length; ++i) {
      if (
        buffer[i] < 0x21 ||
        buffer[i] > 0x7e ||
        buffer[i] === 0x22 ||
        buffer[i] === 0x23 ||
        buffer[i] === 0x3c ||
        buffer[i] === 0x3e
      ) {
        this.url.query += percentEncode(buffer[i]);
      } else {
        this.url.query += String.fromCodePoint(buffer[i]);
      }
    }

    this.buffer = '';
    if (c === 35) {
      this.url.fragment = '';
      this.state = 'fragment';
    }
  } else {
    // TODO: If c is not a URL code point and not "%", parse error.
    if (
      c === 37 &&
      (!isASCIIHex(this.input[this.pointer + 1]) ||
        !isASCIIHex(this.input[this.pointer + 2]))
    ) {
      this.parseError = true;
    }

    this.buffer += cStr;
  }

  return true;
};

URLStateMachine.prototype['parse fragment'] = function parseFragment(c) {
  if (isNaN(c)) {
    // do nothing
  } else if (c === 0x0) {
    this.parseError = true;
  } else {
    // TODO: If c is not a URL code point and not "%", parse error.
    if (
      c === 37 &&
      (!isASCIIHex(this.input[this.pointer + 1]) ||
        !isASCIIHex(this.input[this.pointer + 2]))
    ) {
      this.parseError = true;
    }

    this.url.fragment += percentEncodeChar(c, isC0ControlPercentEncode);
  }

  return true;
};

function serializeURL(url, excludeFragment) {
  let output = url.scheme + ':';
  if (url.host !== null) {
    output += '//';

    if (url.username !== '' || url.password !== '') {
      output += url.username;
      if (url.password !== '') {
        output += ':' + url.password;
      }
      output += '@';
    }

    output += serializeHost(url.host);

    if (url.port !== null) {
      output += ':' + url.port;
    }
  } else if (url.host === null && url.scheme === 'file') {
    output += '//';
  }

  if (url.cannotBeABaseURL) {
    output += url.path[0];
  } else {
    for (const string of url.path) {
      output += '/' + string;
    }
  }

  if (url.query !== null) {
    output += '?' + url.query;
  }

  if (!excludeFragment && url.fragment !== null) {
    output += '#' + url.fragment;
  }

  return output;
}

function serializeOrigin(tuple) {
  let result = tuple.scheme + '://';
  result += serializeHost(tuple.host);

  if (tuple.port !== null) {
    result += ':' + tuple.port;
  }

  return result;
}

module.exports.serializeURL = serializeURL;

module.exports.serializeURLOrigin = function (url) {
  // https://url.spec.whatwg.org/#concept-url-origin
  switch (url.scheme) {
    case 'blob':
      try {
        return module.exports.serializeURLOrigin(
          module.exports.parseURL(url.path[0])
        );
      } catch (e) {
        // serializing an opaque origin returns "null"
        return 'null';
      }
    case 'ftp':
    case 'gopher':
    case 'http':
    case 'https':
    case 'ws':
    case 'wss':
      return serializeOrigin({
        scheme: url.scheme,
        host: url.host,
        port: url.port,
      });
    case 'file':
      // spec says "exercise to the reader", chrome says "file://"
      return 'file://';
    default:
      // serializing an opaque origin returns "null"
      return 'null';
  }
};

module.exports.basicURLParse = function (input, options) {
  if (options === undefined) {
    options = {};
  }

  const usm = new URLStateMachine(
    input,
    options.baseURL,
    options.encodingOverride,
    options.url,
    options.stateOverride
  );
  if (usm.failure) {
    return 'failure';
  }

  return usm.url;
};

module.exports.setTheUsername = function (url, username) {
  url.username = '';
  const decoded = punycode.ucs2.decode(username);
  for (let i = 0; i < decoded.length; ++i) {
    url.username += percentEncodeChar(decoded[i], isUserinfoPercentEncode);
  }
};

module.exports.setThePassword = function (url, password) {
  url.password = '';
  const decoded = punycode.ucs2.decode(password);
  for (let i = 0; i < decoded.length; ++i) {
    url.password += percentEncodeChar(decoded[i], isUserinfoPercentEncode);
  }
};

module.exports.serializeHost = serializeHost;

module.exports.cannotHaveAUsernamePasswordPort =
  cannotHaveAUsernamePasswordPort;

module.exports.serializeInteger = function (integer) {
  return String(integer);
};

module.exports.parseURL = function (input, options) {
  if (options === undefined) {
    options = {};
  }

  // We don't handle blobs, so this just delegates:
  return module.exports.basicURLParse(input, {
    baseURL: options.baseURL,
    encodingOverride: options.encodingOverride,
  });
};