import { Document } from './Document.mjs';
import { Event } from './Event.mjs';
import { EventTarget } from './EventTarget.mjs';
import { CustomEvent } from './CustomEvent.mjs';
import { ErrorEvent } from './ErrorEvent.mjs';
import { PromiseRejectionEvent } from './PromiseRejectionEvent.mjs';
import { ToggleEvent } from './ToggleEvent.mjs';
import { FocusEvent } from './FocusEvent.mjs';
import { ClipboardEvent } from './ClipboardEvent.mjs';
import { Node } from './Node.mjs';
import { ParentNode } from './ParentNode.mjs';
import { ChildNode } from './ChildNode.mjs';
import { Element } from './Element.mjs';
import { HTMLElement } from './HTMLElement.mjs';
import { SVGElement } from './SVGElement.mjs';
import { CharacterData } from './CharacterData.mjs';
import { Text } from './Text.mjs';
import { Comment } from './Comment.mjs';
import { DocumentFragment } from './DocumentFragment.mjs';
import { HTMLTemplateElement } from './HTMLTemplateElement.mjs';
import { CustomElementRegistryImplementation } from './CustomElementRegistry.mjs';
import { MutationObserver } from './MutationObserver.mjs';
import { HOOKS } from './constants.mjs';

class Window extends EventTarget {
  [HOOKS] = {};
  name = '';
  window = this;
  parent = this;
  self = this;
  top = this;
  document = (() => new Document(this))();
  customElements = (() => new CustomElementRegistryImplementation())();
  location = (() => globalThis.location)();
  navigator = (() => globalThis.navigator)();
  Event = (() => Event)();
  ErrorEvent = (() => ErrorEvent)();
  PromiseRejectionEvent = (() => PromiseRejectionEvent)();
  ToggleEvent = (() => ToggleEvent)();
  FocusEvent = (() => FocusEvent)();
  ClipboardEvent = (() => ClipboardEvent)();
  EventTarget = (() => EventTarget)();
  CustomEvent = (() => CustomEvent)();
  Node = (() => Node)();
  ParentNode = (() => ParentNode)();
  ChildNode = (() => ChildNode)();
  DocumentFragment = (() => DocumentFragment)();
  Document = (() => Document)();
  CharacterData = (() => CharacterData)();
  Comment = (() => Comment)();
  Text = (() => Text)();
  Element = (() => Element)();
  HTMLElement = (() => HTMLElement)();
  SVGElement = (() => SVGElement)();
  HTMLTemplateElement = (() => HTMLTemplateElement)();
  MutationObserver = (() => MutationObserver)();
  #currentOnErrorHandler = null;
  #currentOriginalOnErrorHandler = null;
  #currentOnUnhandledRejectionHandler = null;
  get onerror() {
    return this.#currentOriginalOnErrorHandler;
  }
  set onerror(handler) {
    if (this.#currentOnErrorHandler) {
      this.removeEventListener('error', this.#currentOnErrorHandler);
    }
    if (handler && typeof handler === 'function') {
      // the event listener version receives an event object
      // whereas winwow.onerror receives 5 arguments instead
      // we need to wrap the handler to convert the event object to the 5 arguments
      // and also make sure that when window.onerror is read to return the original handler
      // https://developer.mozilla.org/en-US/docs/Web/API/Window/error_event#syntax
      this.#currentOriginalOnErrorHandler = handler;
      this.#currentOnErrorHandler = event => {
        handler(event.message ?? 'Error', event.filename, event.lineno, event.colno, event.error);
      };
      this.addEventListener('error', this.#currentOnErrorHandler);
    } else {
      this.#currentOnErrorHandler = null;
      this.#currentOriginalOnErrorHandler = null;
    }
  }
  get onunhandledrejection() {
    return this.#currentOnUnhandledRejectionHandler;
  }
  set onunhandledrejection(handler) {
    if (this.#currentOnUnhandledRejectionHandler) {
      this.removeEventListener('unhandledrejection', this.#currentOnUnhandledRejectionHandler);
    }
    if (handler && typeof handler === 'function') {
      this.#currentOnUnhandledRejectionHandler = handler;
      this.addEventListener('unhandledrejection', this.#currentOnUnhandledRejectionHandler);
    } else {
      this.#currentOnUnhandledRejectionHandler = null;
    }
  }
  static setGlobal(window) {
    const properties = Object.getOwnPropertyDescriptors(window);
    delete properties.self;
    Object.defineProperties(globalThis, properties);
    if (typeof globalThis.self === 'undefined') {
      Object.defineProperty(globalThis, 'self', {
        value: window,
        configurable: true,
        writable: true,
        enumerable: true
      });
    } else {
      // There can already be a `self`, like when polyfilling the DOM
      // in a Web Worker. In those cases, just mirror all the `Window`
      // properties onto `self`, rather than wholly redefining it.
      Object.defineProperties(self, properties);
    }
  }
  static setGlobalThis(window) {
    for (const property in window) {
      if (window[property] === window) {
        window[property] = globalThis;
      }
    }
    const properties = Object.getOwnPropertyDescriptors(window);
    const eventTargetPrototypeProperties = Object.getOwnPropertyDescriptors(EventTarget.prototype);
    for (const descriptor of Object.values(eventTargetPrototypeProperties)) {
      if (typeof descriptor.value === 'function') {
        descriptor.value = descriptor.value.bind(window);
      }
    }
    Object.defineProperties(globalThis, properties);
    Object.defineProperties(globalThis, eventTargetPrototypeProperties);
  }
}

export { Window };
