text: "use strict";
  const vm = require("vm");
  const webIDLConversions = require("webidl-conversions");
  const { CSSStyleDeclaration } = require("cssstyle");
  const { Performance: RawPerformance } = require("w3c-hr-time");
  const notImplemented = require("./not-implemented");
  const { define, mixin } = require("../utils");
  const Element = require("../living/generated/Element");
  const EventTarget = require("../living/generated/EventTarget");
  const PageTransitionEvent = require("../living/generated/PageTransitionEvent");
  const namedPropertiesWindow = require("../living/named-properties-window");
  const postMessage = require("../living/post-message");
  const DOMException = require("domexception/webidl2js-wrapper");
  const { btoa, atob } = require("abab");
  const idlUtils = require("../living/generated/utils");
  const WebSocketImpl = require("../living/websockets/WebSocket-impl").implementation;
  const BarProp = require("../living/generated/BarProp");
  const Document = require("../living/generated/Document");
  const External = require("../living/generated/External");
  const Navigator = require("../living/generated/Navigator");
  const Performance = require("../living/generated/Performance");
  const Screen = require("../living/generated/Screen");
  const Storage = require("../living/generated/Storage");
  const Selection = require("../living/generated/Selection");
  const reportException = require("../living/helpers/runtime-script-errors");
  const { fireAnEvent } = require("../living/helpers/events");
  const SessionHistory = require("../living/window/SessionHistory");
  const { forEachMatchingSheetRuleOfElement, getResolvedValue, propertiesWithResolvedValueImplemented } =
    require("../living/helpers/style-rules");
  const jsGlobals = require("./js-globals.json");
  
  const GlobalEventHandlersImpl = require("../living/nodes/GlobalEventHandlers-impl").implementation;
  const WindowEventHandlersImpl = require("../living/nodes/WindowEventHandlers-impl").implementation;
  
  // NB: the require() must be after assigning `module.exports` because this require() is circular
  // TODO: this above note might not even be true anymore... figure out the cycle and document it, or clean up.
  module.exports = Window;
  const { installInterfaces } = require("../living/interfaces");
  
  const jsGlobalEntriesToInstall = Object.entries(jsGlobals).filter(([name]) => name in global);
  
  // https://html.spec.whatwg.org/#the-window-object
  function setupWindow(windowInstance, { runScripts }) {
    if (runScripts === "outside-only" || runScripts === "dangerously") {
      contextifyWindow(windowInstance);
  
      // Without this, these globals will only appear to scripts running inside the context using vm.runScript; they will
      // not appear to scripts running from the outside, including to JSDOM implementation code.
      for (const [globalName, globalPropDesc] of jsGlobalEntriesToInstall) {
        const propDesc = { ...globalPropDesc, value: vm.runInContext(globalName, windowInstance) };
        Object.defineProperty(windowInstance, globalName, propDesc);
      }
    } else {
      // Without contextifying the window, none of the globals will exist. So, let's at least alias them from the Node.js
      // context. See https://github.com/jsdom/jsdom/issues/2727 for more background and discussion.
      for (const [globalName, globalPropDesc] of jsGlobalEntriesToInstall) {
        const propDesc = { ...globalPropDesc, value: global[globalName] };
        Object.defineProperty(windowInstance, globalName, propDesc);
      }
    }
  
    installInterfaces(windowInstance);
  
    const EventTargetConstructor = windowInstance.EventTarget;
  
    // eslint-disable-next-line func-name-matching, func-style, no-shadow
    const windowConstructor = function Window() {
      throw new TypeError("Illegal constructor");
    };
    Object.setPrototypeOf(windowConstructor, EventTargetConstructor);
  
    Object.defineProperty(windowInstance, "Window", {
      configurable: true,
      writable: true,
      value: windowConstructor
    });
  
    const windowPrototype = Object.create(EventTargetConstructor.prototype);
    Object.defineProperties(windowPrototype, {
      constructor: {
        value: windowConstructor,
        writable: true,
        configurable: true
      },
      [Symbol.toStringTag]: {
        value: "Window",
        configurable: true
      }
    });
  
    windowConstructor.prototype = windowPrototype;
    Object.setPrototypeOf(windowInstance, windowPrototype);
  
    EventTarget.setup(windowInstance, windowInstance);
    mixin(windowInstance, WindowEventHandlersImpl.prototype);
    mixin(windowInstance, GlobalEventHandlersImpl.prototype);
    windowInstance._initGlobalEvents();
  
    windowInstance._globalObject = windowInstance;
  }
  
  // NOTE: per https://heycam.github.io/webidl/#Global, all properties on the Window object must be own-properties.
  // That is why we assign everything inside of the constructor, instead of using a shared prototype.
  // You can verify this in e.g. Firefox or Internet Explorer, which do a good job with Web IDL compliance.
  function Window(options) {
    setupWindow(this, { runScripts: options.runScripts });
  
    const rawPerformance = new RawPerformance();
    const windowInitialized = rawPerformance.now();
  
    const window = this;
  
    ///// PRIVATE DATA PROPERTIES
  
    this._resourceLoader = options.resourceLoader;
  
    // vm initialization is deferred until script processing is activated
    this._globalProxy = this;
    Object.defineProperty(idlUtils.implForWrapper(this), idlUtils.wrapperSymbol, { get: () => this._globalProxy });
  
    let timers = Object.create(null);
    let animationFrameCallbacks = Object.create(null);
  
    // List options explicitly to be clear which are passed through
    this._document = Document.create(window, [], {
      options: {
        parsingMode: options.parsingMode,
        contentType: options.contentType,
        encoding: options.encoding,
        cookieJar: options.cookieJar,
        url: options.url,
        lastModified: options.lastModified,
        referrer: options.referrer,
        concurrentNodeIterators: options.concurrentNodeIterators,
        parseOptions: options.parseOptions,
        defaultView: this._globalProxy,
        global: this
      }
    });
  
    if (vm.isContext(window)) {
      const documentImpl = idlUtils.implForWrapper(window._document);
      documentImpl._defaultView = window._globalProxy = vm.runInContext("this", window);
    }
  
    const documentOrigin = idlUtils.implForWrapper(this._document)._origin;
    this._origin = documentOrigin;
  
    // https://html.spec.whatwg.org/#session-history
    this._sessionHistory = new SessionHistory({
      document: idlUtils.implForWrapper(this._document),
      url: idlUtils.implForWrapper(this._document)._URL,
      stateObject: null
    }, this);
  
    this._virtualConsole = options.virtualConsole;
  
    this._runScripts = options.runScripts;
  
    // Set up the window as if it's a top level window.
    // If it's not, then references will be corrected by frame/iframe code.
    this._parent = this._top = this._globalProxy;
    this._frameElement = null;
  
    // This implements window.frames.length, since window.frames returns a
    // self reference to the window object.  This value is incremented in the
    // HTMLFrameElement implementation.
    this._length = 0;
  
    this._pretendToBeVisual = options.pretendToBeVisual;
    this._storageQuota = options.storageQuota;
  
    // Some properties (such as localStorage and sessionStorage) share data
    // between windows in the same origin. This object is intended
    // to contain such data.
    if (options.commonForOrigin && options.commonForOrigin[documentOrigin]) {
      this._commonForOrigin = options.commonForOrigin;
    } else {
      this._commonForOrigin = {
        [documentOrigin]: {
          localStorageArea: new Map(),
          sessionStorageArea: new Map(),
          windowsInSameOrigin: [this]
        }
      };
    }
  
    this._currentOriginData = this._commonForOrigin[documentOrigin];
  
    ///// WEB STORAGE
  
    this._localStorage = Storage.create(window, [], {
      associatedWindow: this,
      storageArea: this._currentOriginData.localStorageArea,
      type: "localStorage",
      url: this._document.documentURI,
      storageQuota: this._storageQuota
    });
    this._sessionStorage = Storage.create(window, [], {
      associatedWindow: this,
      storageArea: this._currentOriginData.sessionStorageArea,
      type: "sessionStorage",
      url: this._document.documentURI,
      storageQuota: this._storageQuota
    });
  
    ///// SELECTION
  
    // https://w3c.github.io/selection-api/#dfn-selection
    this._selection = Selection.createImpl(window);
  
    // https://w3c.github.io/selection-api/#dom-window
    this.getSelection = function () {
      return window._selection;
    };
  
    ///// GETTERS
  
    const locationbar = BarProp.create(window);
    const menubar = BarProp.create(window);
    const personalbar = BarProp.create(window);
    const scrollbars = BarProp.create(window);
    const statusbar = BarProp.create(window);
    const toolbar = BarProp.create(window);
    const external = External.create(window);
    const navigator = Navigator.create(window, [], { userAgent: this._resourceLoader._userAgent });
    const performance = Performance.create(window, [], { rawPerformance });
    const screen = Screen.create(window);
  
    define(this, {
      get length() {
        return window._length;
      },
      get window() {
        return window._globalProxy;
      },
      get frameElement() {
        return idlUtils.wrapperForImpl(window._frameElement);
      },
      get frames() {
        return window._globalProxy;
      },
      get self() {
        return window._globalProxy;
      },
      get parent() {
        return window._parent;
      },
      get top() {
        return window._top;
      },
      get document() {
        return window._document;
      },
      get external() {
        return external;
      },
      get location() {
        return idlUtils.wrapperForImpl(idlUtils.implForWrapper(window._document)._location);
      },
      get history() {
        return idlUtils.wrapperForImpl(idlUtils.implForWrapper(window._document)._history);
      },
      get navigator() {
        return navigator;
      },
      get locationbar() {
        return locationbar;
      },
      get menubar() {
        return menubar;
      },
      get personalbar() {
        return personalbar;
      },
      get scrollbars() {
        return scrollbars;
      },
      get statusbar() {
        return statusbar;
      },
      get toolbar() {
        return toolbar;
      },
      get performance() {
        return performance;
      },
      get screen() {
        return screen;
      },
      get origin() {
        return window._origin;
      },
      // The origin IDL attribute is defined with [Replaceable].
      set origin(value) {
        Object.defineProperty(this, "origin", {
          value,
          writable: true,
          enumerable: true,
          configurable: true
        });
      },
      get localStorage() {
        if (idlUtils.implForWrapper(this._document)._origin === "null") {
          throw DOMException.create(window, [
            "localStorage is not available for opaque origins",
            "SecurityError"
          ]);
        }
  
        return this._localStorage;
      },
      get sessionStorage() {
        if (idlUtils.implForWrapper(this._document)._origin === "null") {
          throw DOMException.create(window, [
            "sessionStorage is not available for opaque origins",
            "SecurityError"
          ]);
        }
  
        return this._sessionStorage;
      }
    });
  
    namedPropertiesWindow.initializeWindow(this, this._globalProxy);
  
    ///// METHODS for [ImplicitThis] hack
    // See https://lists.w3.org/Archives/Public/public-script-coord/2015JanMar/0109.html
    this.addEventListener = this.addEventListener.bind(this);
    this.removeEventListener = this.removeEventListener.bind(this);
    this.dispatchEvent = this.dispatchEvent.bind(this);
  
    ///// METHODS
  
    let latestTimerId = 0;
    let latestAnimationFrameCallbackId = 0;
  
    this.setTimeout = function (fn, ms) {
      const args = [];
      for (let i = 2; i < arguments.length; ++i) {
        args[i - 2] = arguments[i];
      }
      return startTimer(window, setTimeout, clearTimeout, ++latestTimerId, fn, ms, timers, args);
    };
    this.setInterval = function (fn, ms) {
      const args = [];
      for (let i = 2; i < arguments.length; ++i) {
        args[i - 2] = arguments[i];
      }
      return startTimer(window, setInterval, clearInterval, ++latestTimerId, fn, ms, timers, args);
    };
    this.clearInterval = stopTimer.bind(this, timers);
    this.clearTimeout = stopTimer.bind(this, timers);
  
    if (this._pretendToBeVisual) {
      this.requestAnimationFrame = fn => {
        const timestamp = rawPerformance.now() - windowInitialized;
        const fps = 1000 / 60;
  
        return startTimer(
          window,
          setTimeout,
          clearTimeout,
          ++latestAnimationFrameCallbackId,
          fn,
          fps,
          animationFrameCallbacks,
          [timestamp]
        );
      };
      this.cancelAnimationFrame = stopTimer.bind(this, animationFrameCallbacks);
    }
  
    this.__stopAllTimers = function () {
      stopAllTimers(timers);
      stopAllTimers(animationFrameCallbacks);
  
      latestTimerId = 0;
      latestAnimationFrameCallbackId = 0;
  
      timers = Object.create(null);
      animationFrameCallbacks = Object.create(null);
    };
  
    function Option(text, value, defaultSelected, selected) {
      if (text === undefined) {
        text = "";
      }
      text = webIDLConversions.DOMString(text);
  
      if (value !== undefined) {
        value = webIDLConversions.DOMString(value);
      }
  
      defaultSelected = webIDLConversions.boolean(defaultSelected);
      selected = webIDLConversions.boolean(selected);
  
      const option = window._document.createElement("option");
      const impl = idlUtils.implForWrapper(option);
  
      if (text !== "") {
        impl.text = text;
      }
      if (value !== undefined) {
        impl.setAttributeNS(null, "value", value);
      }
      if (defaultSelected) {
        impl.setAttributeNS(null, "selected", "");
      }
      impl._selectedness = selected;
  
      return option;
    }
    Object.defineProperty(Option, "prototype", {
      value: this.HTMLOptionElement.prototype,
      configurable: false,
      enumerable: false,
      writable: false
    });
    Object.defineProperty(window, "Option", {
      value: Option,
      configurable: true,
      enumerable: false,
      writable: true
    });
  
    function Image() {
      const img = window._document.createElement("img");
      const impl = idlUtils.implForWrapper(img);
  
      if (arguments.length > 0) {
        impl.setAttributeNS(null, "width", String(arguments[0]));
      }
      if (arguments.length > 1) {
        impl.setAttributeNS(null, "height", String(arguments[1]));
      }
  
      return img;
    }
    Object.defineProperty(Image, "prototype", {
      value: this.HTMLImageElement.prototype,
      configurable: false,
      enumerable: false,
      writable: false
    });
    Object.defineProperty(window, "Image", {
      value: Image,
      configurable: true,
      enumerable: false,
      writable: true
    });
  
    function Audio(src) {
      const audio = window._document.createElement("audio");
      const impl = idlUtils.implForWrapper(audio);
      impl.setAttributeNS(null, "preload", "auto");
  
      if (src !== undefined) {
        impl.setAttributeNS(null, "src", String(src));
      }
  
      return audio;
    }
    Object.defineProperty(Audio, "prototype", {
      value: this.HTMLAudioElement.prototype,
      configurable: false,
      enumerable: false,
      writable: false
    });
    Object.defineProperty(window, "Audio", {
      value: Audio,
      configurable: true,
      enumerable: false,
      writable: true
    });
  
    this.postMessage = postMessage(window);
  
    this.atob = function (str) {
      const result = atob(str);
      if (result === null) {
        throw DOMException.create(window, [
          "The string to be decoded contains invalid characters.",
          "InvalidCharacterError"
        ]);
      }
      return result;
    };
  
    this.btoa = function (str) {
      const result = btoa(str);
      if (result === null) {
        throw DOMException.create(window, [
          "The string to be encoded contains invalid characters.",
          "InvalidCharacterError"
        ]);
      }
      return result;
    };
  
    this.stop = function () {
      const manager = idlUtils.implForWrapper(this._document)._requestManager;
      if (manager) {
        manager.close();
      }
    };
  
    this.close = function () {
      // Recursively close child frame windows, then ourselves.
      const currentWindow = this;
      (function windowCleaner(windowToClean) {
        for (let i = 0; i < windowToClean.length; i++) {
          windowCleaner(windowToClean[i]);
        }
  
        // We"re already in our own window.close().
        if (windowToClean !== currentWindow) {
          windowToClean.close();
        }
      }(this));
  
      // Clear out all listeners. Any in-flight or upcoming events should not get delivered.
      idlUtils.implForWrapper(this)._eventListeners = Object.create(null);
  
      if (this._document) {
        if (this._document.body) {
          this._document.body.innerHTML = "";
        }
  
        if (this._document.close) {
          // It's especially important to clear out the listeners here because document.close() causes a "load" event to
          // fire.
          idlUtils.implForWrapper(this._document)._eventListeners = Object.create(null);
          this._document.close();
        }
        const doc = idlUtils.implForWrapper(this._document);
        if (doc._requestManager) {
          doc._requestManager.close();
        }
        delete this._document;
      }
  
      this.__stopAllTimers();
      WebSocketImpl.cleanUpWindow(this);
    };
  
    this.getComputedStyle = function (elt) {
      elt = Element.convert(elt);
  
      const declaration = new CSSStyleDeclaration();
      const { forEach } = Array.prototype;
      const { style } = elt;
  
      forEachMatchingSheetRuleOfElement(elt, rule => {
        forEach.call(rule.style, property => {
          declaration.setProperty(
            property,
            rule.style.getPropertyValue(property),
            rule.style.getPropertyPriority(property)
          );
        });
      });
  
      // https://drafts.csswg.org/cssom/#dom-window-getcomputedstyle
      const declarations = Object.keys(propertiesWithResolvedValueImplemented);
      forEach.call(declarations, property => {
        declaration.setProperty(property, getResolvedValue(elt, property));
      });
  
      forEach.call(style, property => {
        declaration.setProperty(property, style.getPropertyValue(property), style.getPropertyPriority(property));
      });
  
      return declaration;
    };
  
    this.getSelection = function () {
      return window._document.getSelection();
    };
  
    // The captureEvents() and releaseEvents() methods must do nothing
    this.captureEvents = function () {};
  
    this.releaseEvents = function () {};
  
    ///// PUBLIC DATA PROPERTIES (TODO: should be getters)
  
    function wrapConsoleMethod(method) {
      return (...args) => {
        window._virtualConsole.emit(method, ...args);
      };
    }
  
    this.console = {
      assert: wrapConsoleMethod("assert"),
      clear: wrapConsoleMethod("clear"),
      count: wrapConsoleMethod("count"),
      countReset: wrapConsoleMethod("countReset"),
      debug: wrapConsoleMethod("debug"),
      dir: wrapConsoleMethod("dir"),
      dirxml: wrapConsoleMethod("dirxml"),
      error: wrapConsoleMethod("error"),
      group: wrapConsoleMethod("group"),
      groupCollapsed: wrapConsoleMethod("groupCollapsed"),
      groupEnd: wrapConsoleMethod("groupEnd"),
      info: wrapConsoleMethod("info"),
      log: wrapConsoleMethod("log"),
      table: wrapConsoleMethod("table"),
      time: wrapConsoleMethod("time"),
      timeEnd: wrapConsoleMethod("timeEnd"),
      trace: wrapConsoleMethod("trace"),
      warn: wrapConsoleMethod("warn")
    };
  
    function notImplementedMethod(name) {
      return function () {
        notImplemented(name, window);
      };
    }
  
    define(this, {
      name: "",
      status: "",
      devicePixelRatio: 1,
      innerWidth: 1024,
      innerHeight: 768,
      outerWidth: 1024,
      outerHeight: 768,
      pageXOffset: 0,
      pageYOffset: 0,
      screenX: 0,
      screenLeft: 0,
      screenY: 0,
      screenTop: 0,
      scrollX: 0,
      scrollY: 0,
  
      alert: notImplementedMethod("window.alert"),
      blur: notImplementedMethod("window.blur"),
      confirm: notImplementedMethod("window.confirm"),
      focus: notImplementedMethod("window.focus"),
      moveBy: notImplementedMethod("window.moveBy"),
      moveTo: notImplementedMethod("window.moveTo"),
      open: notImplementedMethod("window.open"),
      print: notImplementedMethod("window.print"),
      prompt: notImplementedMethod("window.prompt"),
      resizeBy: notImplementedMethod("window.resizeBy"),
      resizeTo: notImplementedMethod("window.resizeTo"),
      scroll: notImplementedMethod("window.scroll"),
      scrollBy: notImplementedMethod("window.scrollBy"),
      scrollTo: notImplementedMethod("window.scrollTo")
    });
  
    ///// INITIALIZATION
  
    process.nextTick(() => {
      if (!window.document) {
        return; // window might've been closed already
      }
  
      if (window.document.readyState === "complete") {
        fireAnEvent("load", window, undefined, {}, window.document);
      } else {
        window.document.addEventListener("load", () => {
          fireAnEvent("load", window, undefined, {}, window.document);
  
          if (!idlUtils.implForWrapper(window._document)._pageShowingFlag) {
            idlUtils.implForWrapper(window._document)._pageShowingFlag = true;
            fireAnEvent("pageshow", window, PageTransitionEvent, { persisted: false }, window.document);
          }
        });
      }
    });
  }
  
  function startTimer(window, startFn, stopFn, timerId, callback, ms, timerStorage, args) {
    if (!window || !window._document) {
      return undefined;
    }
    if (typeof callback !== "function") {
      const code = String(callback);
      callback = window._globalProxy.eval.bind(window, code + `\n//# sourceURL=${window.location.href}`);
    }
  
    const oldCallback = callback;
    callback = () => {
      try {
        oldCallback.apply(window._globalProxy, args);
      } catch (e) {
        reportException(window, e, window.location.href);
      }
      delete timerStorage[timerId];
    };
  
    const res = startFn(callback, ms);
    timerStorage[timerId] = [res, stopFn];
    return timerId;
  }
  
  function stopTimer(timerStorage, id) {
    const timer = timerStorage[id];
    if (timer) {
      // Need to .call() with undefined to ensure the thisArg is not timer itself
      timer[1].call(undefined, timer[0]);
      delete timerStorage[id];
    }
  }
  
  function stopAllTimers(timers) {
    Object.keys(timers).forEach(key => {
      const timer = timers[key];
      // Need to .call() with undefined to ensure the thisArg is not timer itself
      timer[1].call(undefined, timer[0]);
    });
  }
  
  function contextifyWindow(window) {
    if (vm.isContext(window)) {
      return;
    }
  
    vm.createContext(window);
  }
  