// Copyright 2018-2026 the Deno authors. MIT license.

// This module follows most of the WHATWG Living Standard for the DOM logic.
// Many parts of the DOM are not implemented in Deno, but the logic for those
// parts still exists.  This means you will observe a lot of strange structures
// and impossible logic branches based on what Deno currently supports.

(function () {
const { core, internals, primordials } = __bootstrap;
const {
  ArrayPrototypeIncludes,
  ArrayPrototypeIndexOf,
  ArrayPrototypeMap,
  ArrayPrototypePush,
  ArrayPrototypeSlice,
  ArrayPrototypeSplice,
  ArrayPrototypeUnshift,
  Boolean,
  Error,
  FunctionPrototypeCall,
  JSONStringify,
  MapPrototypeGet,
  MapPrototypeSet,
  ObjectCreate,
  ObjectDefineProperty,
  ObjectFreeze,
  ObjectGetOwnPropertyDescriptor,
  ObjectGetPrototypeOf,
  ObjectPrototype,
  ObjectPrototypeIsPrototypeOf,
  ReflectDefineProperty,
  SafeArrayIterator,
  SafeMap,
  String,
  StringPrototypeStartsWith,
  Symbol,
  SymbolFor,
  SymbolIterator,
  SymbolToStringTag,
  TypeError,
} = primordials;

const webidl = core.loadExtScript("ext:deno_webidl/00_webidl.js");
const { DOMException } = core.loadExtScript("ext:deno_web/01_dom_exception.js");

// Lazy-load createFilteredInspectProxy from console to avoid
// circular dependency at load time. Only needed for custom inspect.
let _createFilteredInspectProxy;
function getCreateFilteredInspectProxy() {
  if (!_createFilteredInspectProxy) {
    _createFilteredInspectProxy = core.loadExtScript(
      "ext:deno_web/01_console.js",
    ).createFilteredInspectProxy;
  }
  return _createFilteredInspectProxy;
}

// This should be set via setGlobalThis this is required so that if even
// user deletes globalThis it is still usable
let globalThis_;

function saveGlobalThisReference(val) {
  globalThis_ = val;
}

// accessors for non runtime visible data

function getDispatched(event) {
  return Boolean(event[_dispatched]);
}

function getPath(event) {
  return event[_path] ?? [];
}

function getStopImmediatePropagation(event) {
  return Boolean(event[_stopImmediatePropagationFlag]);
}

function setCurrentTarget(
  event,
  value,
) {
  event[_attributes].currentTarget = value;
}

function setIsTrusted(event, value) {
  event[_isTrusted] = value;
}

function setDispatched(event, value) {
  event[_dispatched] = value;
}

function setEventPhase(event, value) {
  event[_attributes].eventPhase = value;
}

function setInPassiveListener(event, value) {
  event[_inPassiveListener] = value;
}

function setPath(event, value) {
  event[_path] = value;
}

function setRelatedTarget(
  event,
  value,
) {
  event[_attributes].relatedTarget = value;
}

function setTarget(event, value) {
  event[_attributes].target = value;
}

function setStopImmediatePropagation(
  event,
  value,
) {
  event[_stopImmediatePropagationFlag] = value;
}

const isTrusted = ObjectGetOwnPropertyDescriptor({
  get isTrusted() {
    return this[_isTrusted];
  },
}, "isTrusted").get;

const _attributes = Symbol("[[attributes]]");
const _canceledFlag = Symbol("[[canceledFlag]]");
const _stopPropagationFlag = Symbol("[[stopPropagationFlag]]");
const _stopImmediatePropagationFlag = Symbol(
  "[[stopImmediatePropagationFlag]]",
);
const _inPassiveListener = Symbol("[[inPassiveListener]]");
const _dispatched = Symbol("[[dispatched]]");
const _isTrusted = Symbol("[[isTrusted]]");
const _path = Symbol("[[path]]");

class Event {
  constructor(type, eventInitDict = { __proto__: null }) {
    this[_canceledFlag] = false;
    this[_stopPropagationFlag] = false;
    this[_stopImmediatePropagationFlag] = false;
    this[_inPassiveListener] = false;
    this[_dispatched] = false;
    this[_isTrusted] = false;
    this[_path] = [];

    webidl.requiredArguments(
      arguments.length,
      1,
      "Failed to construct 'Event'",
    );
    type = webidl.converters.DOMString(
      type,
      "Failed to construct 'Event'",
      "Argument 1",
    );

    this[_attributes] = {
      type,
      bubbles: !!eventInitDict.bubbles,
      cancelable: !!eventInitDict.cancelable,
      composed: !!eventInitDict.composed,
      currentTarget: null,
      eventPhase: Event.NONE,
      target: null,
      timeStamp: 0,
    };
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return inspect(
      getCreateFilteredInspectProxy()({
        object: this,
        evaluate: ObjectPrototypeIsPrototypeOf(EventPrototype, this),
        keys: EVENT_PROPS,
      }),
      inspectOptions,
    );
  }

  get type() {
    return this[_attributes].type;
  }

  get target() {
    return this[_attributes].target;
  }

  get srcElement() {
    return null;
  }

  set srcElement(_) {
    // this member is deprecated
  }

  get currentTarget() {
    return this[_attributes].currentTarget;
  }

  composedPath() {
    const path = this[_path];
    if (path.length === 0) {
      return [];
    }

    if (!this.currentTarget) {
      throw new Error("assertion error");
    }
    const composedPath = [
      {
        item: this.currentTarget,
        itemInShadowTree: false,
        relatedTarget: null,
        rootOfClosedTree: false,
        slotInClosedTree: false,
        target: null,
        touchTargetList: [],
      },
    ];

    let currentTargetIndex = 0;
    let currentTargetHiddenSubtreeLevel = 0;

    for (let index = path.length - 1; index >= 0; index--) {
      const { item, rootOfClosedTree, slotInClosedTree } = path[index];

      if (rootOfClosedTree) {
        currentTargetHiddenSubtreeLevel++;
      }

      if (item === this.currentTarget) {
        currentTargetIndex = index;
        break;
      }

      if (slotInClosedTree) {
        currentTargetHiddenSubtreeLevel--;
      }
    }

    let currentHiddenLevel = currentTargetHiddenSubtreeLevel;
    let maxHiddenLevel = currentTargetHiddenSubtreeLevel;

    for (let i = currentTargetIndex - 1; i >= 0; i--) {
      const { item, rootOfClosedTree, slotInClosedTree } = path[i];

      if (rootOfClosedTree) {
        currentHiddenLevel++;
      }

      if (currentHiddenLevel <= maxHiddenLevel) {
        ArrayPrototypeUnshift(composedPath, {
          item,
          itemInShadowTree: false,
          relatedTarget: null,
          rootOfClosedTree: false,
          slotInClosedTree: false,
          target: null,
          touchTargetList: [],
        });
      }

      if (slotInClosedTree) {
        currentHiddenLevel--;

        if (currentHiddenLevel < maxHiddenLevel) {
          maxHiddenLevel = currentHiddenLevel;
        }
      }
    }

    currentHiddenLevel = currentTargetHiddenSubtreeLevel;
    maxHiddenLevel = currentTargetHiddenSubtreeLevel;

    for (let index = currentTargetIndex + 1; index < path.length; index++) {
      const { item, rootOfClosedTree, slotInClosedTree } = path[index];

      if (slotInClosedTree) {
        currentHiddenLevel++;
      }

      if (currentHiddenLevel <= maxHiddenLevel) {
        ArrayPrototypePush(composedPath, {
          item,
          itemInShadowTree: false,
          relatedTarget: null,
          rootOfClosedTree: false,
          slotInClosedTree: false,
          target: null,
          touchTargetList: [],
        });
      }

      if (rootOfClosedTree) {
        currentHiddenLevel--;

        if (currentHiddenLevel < maxHiddenLevel) {
          maxHiddenLevel = currentHiddenLevel;
        }
      }
    }
    return ArrayPrototypeMap(composedPath, (p) => p.item);
  }

  get NONE() {
    return Event.NONE;
  }

  get CAPTURING_PHASE() {
    return Event.CAPTURING_PHASE;
  }

  get AT_TARGET() {
    return Event.AT_TARGET;
  }

  get BUBBLING_PHASE() {
    return Event.BUBBLING_PHASE;
  }

  get eventPhase() {
    return this[_attributes].eventPhase;
  }

  stopPropagation() {
    this[_stopPropagationFlag] = true;
  }

  get cancelBubble() {
    return this[_stopPropagationFlag];
  }

  set cancelBubble(value) {
    this[_stopPropagationFlag] = webidl.converters.boolean(value);
  }

  stopImmediatePropagation() {
    this[_stopPropagationFlag] = true;
    this[_stopImmediatePropagationFlag] = true;
  }

  get bubbles() {
    return this[_attributes].bubbles;
  }

  get cancelable() {
    return this[_attributes].cancelable;
  }

  get returnValue() {
    return !this[_canceledFlag];
  }

  set returnValue(value) {
    if (!webidl.converters.boolean(value)) {
      if (this[_attributes].cancelable && !this[_inPassiveListener]) {
        this[_canceledFlag] = true;
      }
    }
  }

  preventDefault() {
    if (this[_attributes].cancelable && !this[_inPassiveListener]) {
      this[_canceledFlag] = true;
    }
  }

  get defaultPrevented() {
    return this[_canceledFlag];
  }

  get composed() {
    return this[_attributes].composed;
  }

  get initialized() {
    return true;
  }

  get timeStamp() {
    return this[_attributes].timeStamp;
  }
}

ObjectDefineProperty(Event, "NONE", {
  __proto__: null,
  value: 0,
  writable: false,
  enumerable: true,
  configurable: false,
});

ObjectDefineProperty(Event, "CAPTURING_PHASE", {
  __proto__: null,
  value: 1,
  writable: false,
  enumerable: true,
  configurable: false,
});

ObjectDefineProperty(Event, "AT_TARGET", {
  __proto__: null,
  value: 2,
  writable: false,
  enumerable: true,
  configurable: false,
});

ObjectDefineProperty(Event, "BUBBLING_PHASE", {
  __proto__: null,
  value: 3,
  writable: false,
  enumerable: true,
  configurable: false,
});

const EventPrototype = Event.prototype;

ObjectDefineProperty(EventPrototype, SymbolToStringTag, {
  __proto__: null,
  value: "Event",
  writable: false,
  enumerable: false,
  configurable: true,
});

// Not spec compliant. The spec defines it as [LegacyUnforgeable]
// but doing so has a big performance hit
ReflectDefineProperty(Event.prototype, "isTrusted", {
  __proto__: null,
  enumerable: true,
  get: isTrusted,
});

function defineEnumerableProps(
  Ctor,
  props,
) {
  for (let i = 0; i < props.length; ++i) {
    const prop = props[i];
    ReflectDefineProperty(Ctor.prototype, prop, {
      __proto__: null,
      enumerable: true,
    });
  }
}

const EVENT_PROPS = [
  "bubbles",
  "cancelable",
  "composed",
  "currentTarget",
  "defaultPrevented",
  "eventPhase",
  "srcElement",
  "target",
  "returnValue",
  "timeStamp",
  "type",
];

defineEnumerableProps(Event, EVENT_PROPS);

// This is currently the only node type we are using, so instead of implementing
// the whole of the Node interface at the moment, this just gives us the one
// value to power the standards based logic
const DOCUMENT_FRAGMENT_NODE = 11;

// DOM Logic Helper functions and type guards

/** Get the parent node, for event targets that have a parent.
 *
 * Ref: https://dom.spec.whatwg.org/#get-the-parent */
function getParent(eventTarget) {
  return isNode(eventTarget) ? eventTarget.parentNode : null;
}

function getRoot(eventTarget) {
  return isNode(eventTarget)
    ? eventTarget.getRootNode({ composed: true })
    : null;
}

function isNode(
  eventTarget,
) {
  return eventTarget?.nodeType !== undefined;
}

// https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor
function isShadowInclusiveAncestor(
  ancestor,
  node,
) {
  while (isNode(node)) {
    if (node === ancestor) {
      return true;
    }

    if (isShadowRoot(node)) {
      node = node && getHost(node);
    } else {
      node = getParent(node);
    }
  }

  return false;
}

function isShadowRoot(nodeImpl) {
  return Boolean(
    nodeImpl &&
      isNode(nodeImpl) &&
      nodeImpl.nodeType === DOCUMENT_FRAGMENT_NODE &&
      getHost(nodeImpl) != null,
  );
}

function isSlottable(
  /* nodeImpl, */
) {
  // TODO(marcosc90) currently there aren't any slottables nodes
  // https://dom.spec.whatwg.org/#concept-slotable
  // return isNode(nodeImpl) && ReflectHas(nodeImpl, "assignedSlot");
  return false;
}

// DOM Logic functions

/** Append a path item to an event's path.
 *
 * Ref: https://dom.spec.whatwg.org/#concept-event-path-append
 */
function appendToEventPath(
  eventImpl,
  target,
  targetOverride,
  relatedTarget,
  touchTargets,
  slotInClosedTree,
) {
  const itemInShadowTree = isNode(target) && isShadowRoot(getRoot(target));
  const rootOfClosedTree = isShadowRoot(target) &&
    getMode(target) === "closed";

  ArrayPrototypePush(getPath(eventImpl), {
    item: target,
    itemInShadowTree,
    target: targetOverride,
    relatedTarget,
    touchTargetList: touchTargets,
    rootOfClosedTree,
    slotInClosedTree,
  });
}

function dispatch(
  targetImpl,
  eventImpl,
  targetOverride,
) {
  let clearTargets = false;
  let activationTarget = null;

  setDispatched(eventImpl, true);

  targetOverride = targetOverride ?? targetImpl;
  const eventRelatedTarget = eventImpl.relatedTarget;
  let relatedTarget = retarget(eventRelatedTarget, targetImpl);

  if (targetImpl !== relatedTarget || targetImpl === eventRelatedTarget) {
    const touchTargets = [];

    appendToEventPath(
      eventImpl,
      targetImpl,
      targetOverride,
      relatedTarget,
      touchTargets,
      false,
    );

    const isActivationEvent = eventImpl.type === "click";

    if (isActivationEvent && getHasActivationBehavior(targetImpl)) {
      activationTarget = targetImpl;
    }

    let slotInClosedTree = false;
    let slottable = isSlottable(targetImpl) && getAssignedSlot(targetImpl)
      ? targetImpl
      : null;
    let parent = getParent(targetImpl);

    // Populate event path
    // https://dom.spec.whatwg.org/#event-path
    while (parent !== null) {
      if (slottable !== null) {
        slottable = null;

        const parentRoot = getRoot(parent);
        if (
          isShadowRoot(parentRoot) &&
          parentRoot &&
          getMode(parentRoot) === "closed"
        ) {
          slotInClosedTree = true;
        }
      }

      relatedTarget = retarget(eventRelatedTarget, parent);

      if (
        isNode(parent) &&
        isShadowInclusiveAncestor(getRoot(targetImpl), parent)
      ) {
        appendToEventPath(
          eventImpl,
          parent,
          null,
          relatedTarget,
          touchTargets,
          slotInClosedTree,
        );
      } else if (parent === relatedTarget) {
        parent = null;
      } else {
        targetImpl = parent;

        if (
          isActivationEvent &&
          activationTarget === null &&
          getHasActivationBehavior(targetImpl)
        ) {
          activationTarget = targetImpl;
        }

        appendToEventPath(
          eventImpl,
          parent,
          targetImpl,
          relatedTarget,
          touchTargets,
          slotInClosedTree,
        );
      }

      if (parent !== null) {
        parent = getParent(parent);
      }

      slotInClosedTree = false;
    }

    let clearTargetsTupleIndex = -1;
    const path = getPath(eventImpl);
    for (
      let i = path.length - 1;
      i >= 0 && clearTargetsTupleIndex === -1;
      i--
    ) {
      if (path[i].target !== null) {
        clearTargetsTupleIndex = i;
      }
    }
    const clearTargetsTuple = path[clearTargetsTupleIndex];

    clearTargets = (isNode(clearTargetsTuple.target) &&
      isShadowRoot(getRoot(clearTargetsTuple.target))) ||
      (isNode(clearTargetsTuple.relatedTarget) &&
        isShadowRoot(getRoot(clearTargetsTuple.relatedTarget)));

    setEventPhase(eventImpl, Event.CAPTURING_PHASE);

    for (let i = path.length - 1; i >= 0; --i) {
      const tuple = path[i];

      if (tuple.target === null) {
        invokeEventListeners(tuple, eventImpl);
      }
    }

    for (let i = 0; i < path.length; i++) {
      const tuple = path[i];

      if (tuple.target !== null) {
        setEventPhase(eventImpl, Event.AT_TARGET);
      } else {
        setEventPhase(eventImpl, Event.BUBBLING_PHASE);
      }

      if (
        (eventImpl.eventPhase === Event.BUBBLING_PHASE &&
          eventImpl.bubbles) ||
        eventImpl.eventPhase === Event.AT_TARGET
      ) {
        invokeEventListeners(tuple, eventImpl);
      }
    }
  }

  setEventPhase(eventImpl, Event.NONE);
  setCurrentTarget(eventImpl, null);
  setPath(eventImpl, []);
  setDispatched(eventImpl, false);
  eventImpl.cancelBubble = false;
  setStopImmediatePropagation(eventImpl, false);

  if (clearTargets) {
    setTarget(eventImpl, null);
    setRelatedTarget(eventImpl, null);
  }

  // TODO(bartlomieju): invoke activation targets if HTML nodes will be implemented
  // if (activationTarget !== null) {
  //   if (!eventImpl.defaultPrevented) {
  //     activationTarget._activationBehavior();
  //   }
  // }

  return !eventImpl.defaultPrevented;
}

/** Inner invoking of the event listeners where the resolved listeners are
 * called.
 *
 * Ref: https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke */
function innerInvokeEventListeners(
  eventImpl,
  targetListeners,
) {
  let found = false;

  const { type } = eventImpl;

  if (!targetListeners || !targetListeners[type]) {
    return found;
  }

  let handlers = targetListeners[type];
  const handlersLength = handlers.length;

  // Copy event listeners before iterating since the list can be modified during the iteration.
  if (handlersLength > 1) {
    handlers = ArrayPrototypeSlice(targetListeners[type]);
  }

  for (let i = 0; i < handlersLength; i++) {
    const listener = handlers[i];

    if (
      getStopImmediatePropagation(eventImpl) &&
      !listener.options[kResistStopImmediatePropagation]
    ) {
      continue;
    }

    let capture, once, passive;
    if (typeof listener.options === "boolean") {
      capture = listener.options;
      once = false;
      passive = false;
    } else {
      capture = listener.options.capture;
      once = listener.options.once;
      passive = listener.options.passive;
    }

    // Check if the event listener has been removed since the listeners has been cloned.
    if (!ArrayPrototypeIncludes(targetListeners[type], listener)) {
      continue;
    }

    found = true;

    if (
      (eventImpl.eventPhase === Event.CAPTURING_PHASE && !capture) ||
      (eventImpl.eventPhase === Event.BUBBLING_PHASE && capture)
    ) {
      continue;
    }

    if (once) {
      ArrayPrototypeSplice(
        targetListeners[type],
        ArrayPrototypeIndexOf(targetListeners[type], listener),
        1,
      );
    }

    if (passive) {
      setInPassiveListener(eventImpl, true);
    }

    try {
      if (typeof listener.callback === "object") {
        if (typeof listener.callback.handleEvent === "function") {
          listener.callback.handleEvent(eventImpl);
        }
      } else {
        FunctionPrototypeCall(
          listener.callback,
          eventImpl.currentTarget,
          eventImpl,
        );
      }
    } catch (error) {
      reportException(error);
    }

    setInPassiveListener(eventImpl, false);
  }

  return found;
}

/** Invokes the listeners on a given event path with the supplied event.
 *
 * Ref: https://dom.spec.whatwg.org/#concept-event-listener-invoke */
function invokeEventListeners(tuple, eventImpl) {
  const path = getPath(eventImpl);
  if (path.length === 1) {
    const t = path[0];
    if (t.target) {
      setTarget(eventImpl, t.target);
    }
  } else {
    const tupleIndex = ArrayPrototypeIndexOf(path, tuple);
    for (let i = tupleIndex; i >= 0; i--) {
      const t = path[i];
      if (t.target) {
        setTarget(eventImpl, t.target);
        break;
      }
    }
  }

  setRelatedTarget(eventImpl, tuple.relatedTarget);

  if (eventImpl.cancelBubble) {
    return;
  }

  setCurrentTarget(eventImpl, tuple.item);

  try {
    innerInvokeEventListeners(eventImpl, getListeners(tuple.item));
  } catch (error) {
    reportException(error);
  }
}

// https://dom.spec.whatwg.org/#dom-eventtarget-removeeventlistener
function normalizeEventHandlerOptions(
  options,
) {
  if (
    typeof options === "boolean" || typeof options === "undefined" ||
    options === null
  ) {
    return {
      capture: Boolean(options),
    };
  } else {
    return {
      capture: Boolean(options.capture),
    };
  }
}

/** Retarget the target following the spec logic.
 *
 * Ref: https://dom.spec.whatwg.org/#retarget */
function retarget(a, b) {
  while (true) {
    if (!isNode(a)) {
      return a;
    }

    const aRoot = a.getRootNode();

    if (aRoot) {
      if (
        !isShadowRoot(aRoot) ||
        (isNode(b) && isShadowInclusiveAncestor(aRoot, b))
      ) {
        return a;
      }

      a = getHost(aRoot);
    }
  }
}

// Accessors for non-public data

const eventTargetData = Symbol();
const kResistStopImmediatePropagation = Symbol(
  "kResistStopImmediatePropagation",
);

function setEventTargetData(target) {
  target[eventTargetData] = getDefaultTargetData();
}

function getAssignedSlot(target) {
  return Boolean(target?.[eventTargetData]?.assignedSlot);
}

function getHasActivationBehavior(target) {
  return Boolean(target?.[eventTargetData]?.hasActivationBehavior);
}

function getHost(target) {
  return target?.[eventTargetData]?.host ?? null;
}

function getListeners(target) {
  return target?.[eventTargetData]?.listeners ?? {};
}

function getMode(target) {
  return target?.[eventTargetData]?.mode ?? null;
}

function listenerCount(target, type) {
  return getListeners(target)?.[type]?.length ?? 0;
}

function getDefaultTargetData() {
  return {
    assignedSlot: false,
    hasActivationBehavior: false,
    host: null,
    listeners: ObjectCreate(null),
    mode: "",
  };
}

function addEventListenerOptionsConverter(V, prefix) {
  if (webidl.type(V) !== "Object") {
    return { capture: !!V, once: false, passive: false };
  }

  const options = {
    capture: !!V.capture,
    once: !!V.once,
    passive: !!V.passive,
    // This field exists for simulating Node.js behavior, implemented in https://github.com/nodejs/node/commit/bcd35c334ec75402ee081f1c4da128c339f70c24
    // Some internal event listeners in Node.js can ignore `e.stopImmediatePropagation()` calls
    // from the earlier event listeners.
    [kResistStopImmediatePropagation]: !!V[kResistStopImmediatePropagation],
  };

  const signal = V.signal;
  if (signal !== undefined) {
    options.signal = webidl.converters.AbortSignal(
      signal,
      prefix,
      "'signal' of 'AddEventListenerOptions' (Argument 3)",
    );
  }

  return options;
}

class EventTarget {
  constructor() {
    // eventTargetData is allocated lazily on first addEventListener (or via
    // setEventTargetData for objects built with webidl.createBranded). Most
    // short-lived EventTarget instances created on the request/response hot
    // path (AbortSignal, transient streams) never have a listener attached
    // and never need the listeners map / 5-field default object.
    this[webidl.brand] = webidl.brand;
  }

  addEventListener(
    type,
    callback,
    options,
  ) {
    const self = this ?? globalThis_;
    webidl.assertBranded(self, EventTargetPrototype);
    const prefix = "Failed to execute 'addEventListener' on 'EventTarget'";

    webidl.requiredArguments(arguments.length, 2, prefix);

    options = addEventListenerOptionsConverter(options, prefix);

    if (callback === null) {
      return;
    }

    let data = self[eventTargetData];
    if (data === undefined) {
      data = self[eventTargetData] = getDefaultTargetData();
    }
    const { listeners } = data;

    if (!listeners[type]) {
      listeners[type] = [];
    }

    const listenerList = listeners[type];
    for (let i = 0; i < listenerList.length; ++i) {
      const listener = listenerList[i];
      if (
        ((typeof listener.options === "boolean" &&
          listener.options === options.capture) ||
          (typeof listener.options === "object" &&
            listener.options.capture === options.capture)) &&
        listener.callback === callback
      ) {
        return;
      }
    }
    if (options?.signal) {
      const signal = options?.signal;
      if (signal.aborted) {
        // If signal is not null and its aborted flag is set, then return.
        return;
      } else {
        // If listener's signal is not null, then add the following abort
        // abort steps to it: Remove an event listener.
        signal.addEventListener("abort", () => {
          self.removeEventListener(type, callback, options);
        });
      }
    }

    ArrayPrototypePush(listeners[type], { callback, options });
  }

  removeEventListener(
    type,
    callback,
    options,
  ) {
    const self = this ?? globalThis_;
    webidl.assertBranded(self, EventTargetPrototype);
    webidl.requiredArguments(
      arguments.length,
      2,
      "Failed to execute 'removeEventListener' on 'EventTarget'",
    );

    const data = self[eventTargetData];
    if (data === undefined || callback === null || !data.listeners[type]) {
      return;
    }
    const { listeners } = data;

    options = normalizeEventHandlerOptions(options);

    for (let i = 0; i < listeners[type].length; ++i) {
      const listener = listeners[type][i];
      if (
        ((typeof listener.options === "boolean" &&
          listener.options === options.capture) ||
          (typeof listener.options === "object" &&
            listener.options.capture === options.capture)) &&
        listener.callback === callback
      ) {
        ArrayPrototypeSplice(listeners[type], i, 1);
        break;
      }
    }
  }

  dispatchEvent(event) {
    // If `this` is not present, then fallback to global scope. We don't use
    // `globalThis` directly here, because it could be deleted by user.
    // Instead use saved reference to global scope when the script was
    // executed.
    const self = this ?? globalThis_;
    webidl.assertBranded(self, EventTargetPrototype);
    webidl.requiredArguments(
      arguments.length,
      1,
      "Failed to execute 'dispatchEvent' on 'EventTarget'",
    );

    // This is an optimization to avoid creating an event listener
    // on each startup.
    // Stores the flag for checking whether unload is dispatched or not.
    // This prevents the recursive dispatches of unload events.
    // See https://github.com/denoland/deno/issues/9201.
    if (event.type === "unload" && self === globalThis_) {
      globalThis_[SymbolFor("Deno.isUnloadDispatched")] = true;
    }

    const data = self[eventTargetData];
    if (data === undefined || !data.listeners[event.type]) {
      setTarget(event, this);
      return true;
    }

    if (getDispatched(event)) {
      throw new DOMException("Invalid event state", "InvalidStateError");
    }

    if (event.eventPhase !== Event.NONE) {
      throw new DOMException("Invalid event state", "InvalidStateError");
    }

    return dispatch(self, event);
  }

  getParent(_event) {
    return null;
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return `${this.constructor.name} ${inspect({}, inspectOptions)}`;
  }
}

webidl.configureInterface(EventTarget);
const EventTargetPrototype = EventTarget.prototype;

defineEnumerableProps(EventTarget, [
  "addEventListener",
  "removeEventListener",
  "dispatchEvent",
]);

class ErrorEvent extends Event {
  #message = "";
  #filename = "";
  #lineno = "";
  #colno = "";
  #error = "";

  get message() {
    return this.#message;
  }
  get filename() {
    return this.#filename;
  }
  get lineno() {
    return this.#lineno;
  }
  get colno() {
    return this.#colno;
  }
  get error() {
    return this.#error;
  }

  constructor(
    type,
    {
      bubbles,
      cancelable,
      composed,
      message = "",
      filename = "",
      lineno = 0,
      colno = 0,
      error,
    } = { __proto__: null },
  ) {
    super(type, {
      bubbles: bubbles,
      cancelable: cancelable,
      composed: composed,
    });

    this.#message = message;
    this.#filename = filename;
    this.#lineno = lineno;
    this.#colno = colno;
    this.#error = error;
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return inspect(
      getCreateFilteredInspectProxy()({
        object: this,
        evaluate: ObjectPrototypeIsPrototypeOf(ErrorEventPrototype, this),
        keys: [
          ...new SafeArrayIterator(EVENT_PROPS),
          "message",
          "filename",
          "lineno",
          "colno",
          "error",
        ],
      }),
      inspectOptions,
    );
  }

  // TODO(lucacasonato): remove when this interface is spec aligned
  [SymbolToStringTag] = "ErrorEvent";
}

const ErrorEventPrototype = ErrorEvent.prototype;

defineEnumerableProps(ErrorEvent, [
  "message",
  "filename",
  "lineno",
  "colno",
  "error",
]);

class CloseEvent extends Event {
  #wasClean = "";
  #code = "";
  #reason = "";

  get wasClean() {
    return this.#wasClean;
  }
  get code() {
    return this.#code;
  }
  get reason() {
    return this.#reason;
  }

  constructor(type, {
    bubbles,
    cancelable,
    composed,
    wasClean = false,
    code = 0,
    reason = "",
  } = { __proto__: null }) {
    super(type, {
      bubbles: bubbles,
      cancelable: cancelable,
      composed: composed,
    });

    this.#wasClean = wasClean;
    this.#code = code;
    this.#reason = reason;
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return inspect(
      getCreateFilteredInspectProxy()({
        object: this,
        evaluate: ObjectPrototypeIsPrototypeOf(CloseEventPrototype, this),
        keys: [
          ...new SafeArrayIterator(EVENT_PROPS),
          "wasClean",
          "code",
          "reason",
        ],
      }),
      inspectOptions,
    );
  }
}

const CloseEventPrototype = CloseEvent.prototype;

// Lazy reference to MessagePort.prototype - 13_message_port.js depends on
// this module, so the script can't be loaded at definition time without
// causing a load cycle. Cached on first access.
let _MessagePortPrototype;
function getMessagePortPrototype() {
  if (_MessagePortPrototype === undefined) {
    _MessagePortPrototype =
      core.loadExtScript("ext:deno_web/13_message_port.js")
        .MessagePortPrototype;
  }
  return _MessagePortPrototype;
}

// Format an arbitrary JS value for inclusion in a MessageEvent constructor
// error message *for the "must be MessagePort" check*: strings and primitives
// get JSON-quoted (`"null"`, `"1"`, `"{}"`), matching the regex assertions
// Node's tests use for source/ports[i].
function formatForBranded(value) {
  if (value === null) return '"null"';
  if (value === undefined) return '"undefined"';
  const t = typeof value;
  if (t === "string") return JSONStringify(value);
  if (t === "object" || t === "function") return '"{}"';
  return JSONStringify(String(value));
}

// Format for the iterable-check error: the raw value's string representation,
// unquoted. Node's test expects `eventInitDict.ports (0) is not iterable.`
function formatForRaw(value) {
  if (value === null) return "null";
  if (value === undefined) return "undefined";
  const t = typeof value;
  if (t === "string") return JSONStringify(value);
  if (t === "object" || t === "function") return "{}";
  return String(value);
}

class MessageEvent extends Event {
  #source = null;

  get source() {
    return this.#source;
  }

  constructor(type, eventInitDict) {
    super(type, {
      bubbles: eventInitDict?.bubbles ?? false,
      cancelable: eventInitDict?.cancelable ?? false,
      composed: eventInitDict?.composed ?? false,
    });

    this.data = eventInitDict?.data ?? null;
    const ports = eventInitDict?.ports;
    if (ports == null) {
      // `ports` is a FrozenArray<MessagePort> per the HTML spec, so the
      // exposed array must be read-only.
      this.ports = ObjectFreeze([]);
    } else {
      // MessageEvent ports: iterable validation + per-element MessagePort
      // type check. Matches the messages Node asserts on so the
      // node_compat tests pass while still being WHATWG-shaped.
      if (
        ports === null || typeof ports !== "object" ||
        ports[SymbolIterator] === undefined
      ) {
        throw new TypeError(
          `MessageEvent constructor: eventInitDict.ports (${
            formatForRaw(ports)
          }) is not iterable.`,
        );
      }
      const MessagePortProto = getMessagePortPrototype();
      const arr = [];
      let i = 0;
      // Iterate using the user's own iterator so values that aren't real
      // arrays (e.g. a `RegExp` with a custom `Symbol.iterator` -- covered
      // by WPT's no-regexp-special-casing test) still produce the
      // expected `ports` array. SafeArrayIterator can't be used here
      // because it walks the value as if it were an Array.
      // deno-lint-ignore deno-internal/prefer-primordials
      for (const p of ports) {
        if (
          p === null || typeof p !== "object" ||
          !ObjectPrototypeIsPrototypeOf(MessagePortProto, p)
        ) {
          throw new TypeError(
            `MessageEvent constructor: Expected eventInitDict.ports[${i}] (${
              formatForBranded(p)
            }) to be an instance of MessagePort.`,
          );
        }
        arr[i++] = p;
      }
      // `ports` is a FrozenArray<MessagePort> per the HTML spec.
      this.ports = ObjectFreeze(arr);
    }
    // origin and lastEventId are USVString per spec, so coerce to string
    // (Node's test passes numbers and expects string coercion).
    this.origin = eventInitDict?.origin === undefined
      ? ""
      : String(eventInitDict.origin);
    this.lastEventId = eventInitDict?.lastEventId === undefined
      ? ""
      : String(eventInitDict.lastEventId);
    const source = eventInitDict?.source;
    if (source != null) {
      // The Web spec types source as `MessageEventSource = MessagePort |
      // ServiceWorker | WindowProxy`. We don't have a JS-level Window or
      // ServiceWorker brand to gate against, so accept anything except
      // primitives (which the node_compat tests expect to throw on,
      // e.g. `source: 1`) and plain `Object` instances like `{}` (also
      // checked by Node). MessagePort, Window-like globals, and
      // user-defined classes all pass through unchanged -- matching both
      // the WPT `messageevent-constructor.https.html` test (which
      // assigns `window` to `source`) and node_compat
      // `test-worker-message-event`.
      const t = typeof source;
      const isPrimitive = t !== "object" && t !== "function";
      // Treat values whose prototype is exactly Object.prototype (i.e.
      // plain object literals like `{}`) as invalid sources. Using the
      // prototype chain rather than `.constructor` avoids the
      // prefer-primordials lint and is more reliable since user code
      // can override `constructor`.
      const isPlainObject = !isPrimitive &&
        ObjectGetPrototypeOf(source) === ObjectPrototype;
      if (isPrimitive || isPlainObject) {
        throw new TypeError(
          `MessageEvent constructor: Expected eventInitDict.source (${
            formatForBranded(source)
          }) to be an instance of MessagePort.`,
        );
      }
      this.#source = source;
    } else {
      this.#source = null;
    }
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return inspect(
      getCreateFilteredInspectProxy()({
        object: this,
        evaluate: ObjectPrototypeIsPrototypeOf(MessageEventPrototype, this),
        keys: [
          ...new SafeArrayIterator(EVENT_PROPS),
          "data",
          "origin",
          "lastEventId",
        ],
      }),
      inspectOptions,
    );
  }

  // TODO(lucacasonato): remove when this interface is spec aligned
  [SymbolToStringTag] = "MessageEvent";
}

const MessageEventPrototype = MessageEvent.prototype;

class CustomEvent extends Event {
  #detail = null;

  constructor(type, eventInitDict = { __proto__: null }) {
    super(type, eventInitDict);
    webidl.requiredArguments(
      arguments.length,
      1,
      "Failed to construct 'CustomEvent'",
    );
    const { detail } = eventInitDict;
    this.#detail = detail;
  }

  get detail() {
    return this.#detail;
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return inspect(
      getCreateFilteredInspectProxy()({
        object: this,
        evaluate: ObjectPrototypeIsPrototypeOf(CustomEventPrototype, this),
        keys: [
          ...new SafeArrayIterator(EVENT_PROPS),
          "detail",
        ],
      }),
      inspectOptions,
    );
  }

  // TODO(lucacasonato): remove when this interface is spec aligned
  [SymbolToStringTag] = "CustomEvent";
}

const CustomEventPrototype = CustomEvent.prototype;

ReflectDefineProperty(CustomEvent.prototype, "detail", {
  __proto__: null,
  enumerable: true,
});

// ProgressEvent could also be used in other DOM progress event emits.
// Current use is for FileReader.
class ProgressEvent extends Event {
  constructor(type, eventInitDict = { __proto__: null }) {
    super(type, eventInitDict);

    this.lengthComputable = eventInitDict?.lengthComputable ?? false;
    this.loaded = eventInitDict?.loaded ?? 0;
    this.total = eventInitDict?.total ?? 0;
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return inspect(
      getCreateFilteredInspectProxy()({
        object: this,
        evaluate: ObjectPrototypeIsPrototypeOf(ProgressEventPrototype, this),
        keys: [
          ...new SafeArrayIterator(EVENT_PROPS),
          "lengthComputable",
          "loaded",
          "total",
        ],
      }),
      inspectOptions,
    );
  }

  // TODO(lucacasonato): remove when this interface is spec aligned
  [SymbolToStringTag] = "ProgressEvent";
}

const ProgressEventPrototype = ProgressEvent.prototype;

class PromiseRejectionEvent extends Event {
  #promise = null;
  #reason = null;

  get promise() {
    return this.#promise;
  }
  get reason() {
    return this.#reason;
  }

  constructor(
    type,
    {
      bubbles,
      cancelable,
      composed,
      promise,
      reason,
    } = { __proto__: null },
  ) {
    super(type, {
      bubbles: bubbles,
      cancelable: cancelable,
      composed: composed,
    });

    this.#promise = promise;
    this.#reason = reason;
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return inspect(
      getCreateFilteredInspectProxy()({
        object: this,
        evaluate: ObjectPrototypeIsPrototypeOf(
          PromiseRejectionEventPrototype,
          this,
        ),
        keys: [
          ...new SafeArrayIterator(EVENT_PROPS),
          "promise",
          "reason",
        ],
      }),
      inspectOptions,
    );
  }

  // TODO(lucacasonato): remove when this interface is spec aligned
  [SymbolToStringTag] = "PromiseRejectionEvent";
}

const PromiseRejectionEventPrototype = PromiseRejectionEvent.prototype;

defineEnumerableProps(PromiseRejectionEvent, [
  "promise",
  "reason",
]);

const _eventHandlers = Symbol("eventHandlers");

function makeWrappedHandler(handler, isSpecialErrorEventHandler) {
  function wrappedHandler(evt) {
    if (typeof wrappedHandler.handler !== "function") {
      return;
    }

    if (
      isSpecialErrorEventHandler &&
      ObjectPrototypeIsPrototypeOf(ErrorEventPrototype, evt) &&
      evt.type === "error"
    ) {
      const ret = FunctionPrototypeCall(
        wrappedHandler.handler,
        this,
        evt.message,
        evt.filename,
        evt.lineno,
        evt.colno,
        evt.error,
      );
      if (ret === true) {
        evt.preventDefault();
      }
      return;
    }

    return FunctionPrototypeCall(wrappedHandler.handler, this, evt);
  }
  wrappedHandler.handler = handler;
  return wrappedHandler;
}

// `init` is an optional function that will be called the first time that the
// event handler property is set. It will be called with the object on which
// the property is set as its argument.
// `isSpecialErrorEventHandler` can be set to true to opt into the special
// behavior of event handlers for the "error" event in a global scope.
function defineEventHandler(
  emitter,
  name,
  init = undefined,
  isSpecialErrorEventHandler = false,
) {
  // HTML specification section 8.1.7.1
  ObjectDefineProperty(emitter, `on${name}`, {
    __proto__: null,
    get() {
      if (!this[_eventHandlers]) {
        return null;
      }

      return MapPrototypeGet(this[_eventHandlers], name)?.handler ?? null;
    },
    set(value) {
      // All three Web IDL event handler types are nullable callback functions
      // with the [LegacyTreatNonObjectAsNull] extended attribute, meaning
      // anything other than an object is treated as null.
      if (typeof value !== "object" && typeof value !== "function") {
        value = null;
      }

      if (!this[_eventHandlers]) {
        this[_eventHandlers] = new SafeMap();
      }
      let handlerWrapper = MapPrototypeGet(this[_eventHandlers], name);
      if (handlerWrapper) {
        handlerWrapper.handler = value;
      } else if (value !== null) {
        handlerWrapper = makeWrappedHandler(
          value,
          isSpecialErrorEventHandler,
        );
        this.addEventListener(name, handlerWrapper);
        init?.(this);
      }
      MapPrototypeSet(this[_eventHandlers], name, handlerWrapper);
    },
    configurable: true,
    enumerable: true,
  });
}

let reportExceptionStackedCalls = 0;

// https://html.spec.whatwg.org/#report-the-exception
function reportException(error) {
  reportExceptionStackedCalls++;
  const jsError = core.destructureError(error);
  const message = jsError.exceptionMessage;
  let filename = "";
  let lineno = 0;
  let colno = 0;
  if (jsError.frames.length > 0) {
    filename = jsError.frames[0].fileName;
    lineno = jsError.frames[0].lineNumber;
    colno = jsError.frames[0].columnNumber;
  } else {
    const jsError = core.destructureError(new Error());
    const frames = jsError.frames;
    for (let i = 0; i < frames.length; ++i) {
      const frame = frames[i];
      if (
        typeof frame.fileName == "string" &&
        !StringPrototypeStartsWith(frame.fileName, "ext:")
      ) {
        filename = frame.fileName;
        lineno = frame.lineNumber;
        colno = frame.columnNumber;
        break;
      }
    }
  }
  const event = new ErrorEvent("error", {
    cancelable: true,
    message,
    filename,
    lineno,
    colno,
    error,
  });
  // Avoid recursing `reportException()` via error handlers more than once.
  if (reportExceptionStackedCalls > 1 || globalThis_.dispatchEvent(event)) {
    core.reportUnhandledException(error);
  }
  reportExceptionStackedCalls--;
}

function checkThis(thisArg) {
  if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis_) {
    throw new TypeError("Illegal invocation");
  }
}

// https://html.spec.whatwg.org/#dom-reporterror
function reportError(error) {
  checkThis(this);
  const prefix = "Failed to execute 'reportError'";
  webidl.requiredArguments(arguments.length, 1, prefix);
  reportException(error);
}

internals.defineEventHandler = defineEventHandler;
internals.setEventTargetData = setEventTargetData;

return {
  CloseEvent,
  CustomEvent,
  defineEventHandler,
  dispatch,
  ErrorEvent,
  Event,
  eventTargetData,
  EventTarget,
  EventTargetPrototype,
  kResistStopImmediatePropagation,
  listenerCount,
  MessageEvent,
  ProgressEvent,
  PromiseRejectionEvent,
  reportError,
  reportException,
  saveGlobalThisReference,
  setEventTargetData,
  setIsTrusted,
  setTarget,
};
})();
