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

(function () {
const { core, primordials } = __bootstrap;
const { op_now, op_time_origin } = core.ops;
const {
  ArrayIsArray,
  ArrayPrototypeFilter,
  ArrayPrototypeIncludes,
  ArrayPrototypeIndexOf,
  ArrayPrototypePush,
  ArrayPrototypeSlice,
  ArrayPrototypeSplice,
  ObjectKeys,
  ObjectPrototypeIsPrototypeOf,
  queueMicrotask,
  ReflectHas,
  Symbol,
  SymbolFor,
  TypeError,
  TypedArrayPrototypeGetBuffer,
  Uint8Array,
  Uint32Array,
} = primordials;

const webidl = core.loadExtScript("ext:deno_webidl/00_webidl.js");

// Lazy-load structuredClone from 02_structured_clone
let _structuredClone;
function getStructuredClone() {
  if (!_structuredClone) {
    _structuredClone = core.loadExtScript(
      "ext:deno_web/02_structured_clone.js",
    ).structuredClone;
  }
  return _structuredClone;
}

// Lazy-load createFilteredInspectProxy from console
let _createFilteredInspectProxy;
function getCreateFilteredInspectProxy() {
  if (!_createFilteredInspectProxy) {
    _createFilteredInspectProxy = core.loadExtScript(
      "ext:deno_web/01_console.js",
    ).createFilteredInspectProxy;
  }
  return _createFilteredInspectProxy;
}

const { EventTarget } = core.loadExtScript("ext:deno_web/02_event.js");
const { DOMException } = core.loadExtScript("ext:deno_web/01_dom_exception.js");

const illegalConstructorKey = Symbol("illegalConstructorKey");
let performanceEntries = [];
let timeOrigin;
const performanceObservers = [];

const hrU8 = new Uint8Array(8);
const hr = new Uint32Array(TypedArrayPrototypeGetBuffer(hrU8));

function setTimeOrigin() {
  op_time_origin(hrU8);
  timeOrigin = hr[0] * 1000 + hr[1] / 1e6;
}

function now() {
  op_now(hrU8);
  return hr[0] * 1000 + hr[1] / 1e6;
}

webidl.converters["PerformanceMarkOptions"] = webidl
  .createDictionaryConverter(
    "PerformanceMarkOptions",
    [
      {
        key: "detail",
        converter: webidl.converters.any,
      },
      {
        key: "startTime",
        converter: webidl.converters.DOMHighResTimeStamp,
      },
    ],
  );

webidl.converters["DOMString or DOMHighResTimeStamp"] = (
  V,
  prefix,
  context,
  opts,
) => {
  if (webidl.type(V) === "Number" && V !== null) {
    return webidl.converters.DOMHighResTimeStamp(V, prefix, context, opts);
  }
  return webidl.converters.DOMString(V, prefix, context, opts);
};

webidl.converters["PerformanceMeasureOptions"] = webidl
  .createDictionaryConverter(
    "PerformanceMeasureOptions",
    [
      {
        key: "detail",
        converter: webidl.converters.any,
      },
      {
        key: "start",
        converter: webidl.converters["DOMString or DOMHighResTimeStamp"],
      },
      {
        key: "duration",
        converter: webidl.converters.DOMHighResTimeStamp,
      },
      {
        key: "end",
        converter: webidl.converters["DOMString or DOMHighResTimeStamp"],
      },
    ],
  );

webidl.converters["DOMString or PerformanceMeasureOptions"] = (
  V,
  prefix,
  context,
  opts,
) => {
  if (webidl.type(V) === "Object" && V !== null) {
    return webidl.converters["PerformanceMeasureOptions"](
      V,
      prefix,
      context,
      opts,
    );
  }
  return webidl.converters.DOMString(V, prefix, context, opts);
};

function findMostRecent(
  name,
  type,
) {
  for (let i = performanceEntries.length - 1; i >= 0; --i) {
    const entry = performanceEntries[i];
    if (entry.name === name && entry.entryType === type) {
      return entry;
    }
  }
}

function convertMarkToTimestamp(mark) {
  if (typeof mark === "string") {
    const entry = findMostRecent(mark, "mark");
    if (!entry) {
      throw new DOMException(
        `Cannot find mark: "${mark}"`,
        "SyntaxError",
      );
    }
    return entry.startTime;
  }
  if (mark < 0) {
    throw new TypeError(`Mark cannot be negative: received ${mark}`);
  }
  return mark;
}

function filterByNameType(
  name,
  type,
) {
  return ArrayPrototypeFilter(
    performanceEntries,
    (entry) =>
      (name ? entry.name === name : true) &&
      (type ? entry.entryType === type : true),
  );
}

const _name = Symbol("[[name]]");
const _entryType = Symbol("[[entryType]]");
const _startTime = Symbol("[[startTime]]");
const _duration = Symbol("[[duration]]");
class PerformanceEntry {
  [_name] = "";
  [_entryType] = "";
  [_startTime] = 0;
  [_duration] = 0;

  get name() {
    webidl.assertBranded(this, PerformanceEntryPrototype);
    return this[_name];
  }

  get entryType() {
    webidl.assertBranded(this, PerformanceEntryPrototype);
    return this[_entryType];
  }

  get startTime() {
    webidl.assertBranded(this, PerformanceEntryPrototype);
    return this[_startTime];
  }

  get duration() {
    webidl.assertBranded(this, PerformanceEntryPrototype);
    return this[_duration];
  }

  constructor(
    name = null,
    entryType = null,
    startTime = null,
    duration = null,
    key = undefined,
  ) {
    if (key !== illegalConstructorKey) {
      webidl.illegalConstructor();
    }
    this[webidl.brand] = webidl.brand;

    this[_name] = name;
    this[_entryType] = entryType;
    this[_startTime] = startTime;
    this[_duration] = duration;
  }

  toJSON() {
    webidl.assertBranded(this, PerformanceEntryPrototype);
    return {
      name: this[_name],
      entryType: this[_entryType],
      startTime: this[_startTime],
      duration: this[_duration],
    };
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return inspect(
      getCreateFilteredInspectProxy()({
        object: this,
        evaluate: ObjectPrototypeIsPrototypeOf(
          PerformanceEntryPrototype,
          this,
        ),
        keys: [
          "name",
          "entryType",
          "startTime",
          "duration",
        ],
      }),
      inspectOptions,
    );
  }
}
webidl.configureInterface(PerformanceEntry);
const PerformanceEntryPrototype = PerformanceEntry.prototype;

const _detail = Symbol("[[detail]]");
class PerformanceMark extends PerformanceEntry {
  [_detail] = null;

  get detail() {
    webidl.assertBranded(this, PerformanceMarkPrototype);
    return this[_detail];
  }

  get entryType() {
    webidl.assertBranded(this, PerformanceMarkPrototype);
    return "mark";
  }

  constructor(
    name,
    options = { __proto__: null },
  ) {
    const prefix = "Failed to construct 'PerformanceMark'";
    webidl.requiredArguments(arguments.length, 1, prefix);

    name = webidl.converters.DOMString(name, prefix, "Argument 1");

    options = webidl.converters.PerformanceMarkOptions(
      options,
      prefix,
      "Argument 2",
    );

    const { detail = null, startTime = now() } = options;

    super(name, "mark", startTime, 0, illegalConstructorKey);
    this[webidl.brand] = webidl.brand;
    if (startTime < 0) {
      throw new TypeError(
        `Cannot construct PerformanceMark: startTime cannot be negative, received ${startTime}`,
      );
    }
    this[_detail] = getStructuredClone()(detail);
  }

  toJSON() {
    webidl.assertBranded(this, PerformanceMarkPrototype);
    return {
      name: this.name,
      entryType: this.entryType,
      startTime: this.startTime,
      duration: this.duration,
      detail: this.detail,
    };
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return inspect(
      getCreateFilteredInspectProxy()({
        object: this,
        evaluate: ObjectPrototypeIsPrototypeOf(PerformanceMarkPrototype, this),
        keys: [
          "name",
          "entryType",
          "startTime",
          "duration",
          "detail",
        ],
      }),
      inspectOptions,
    );
  }
}
webidl.configureInterface(PerformanceMark);
const PerformanceMarkPrototype = PerformanceMark.prototype;
class PerformanceMeasure extends PerformanceEntry {
  [_detail] = null;

  get detail() {
    webidl.assertBranded(this, PerformanceMeasurePrototype);
    return this[_detail];
  }

  get entryType() {
    webidl.assertBranded(this, PerformanceMeasurePrototype);
    return "measure";
  }

  constructor(
    name = null,
    startTime = null,
    duration = null,
    detail = null,
    key = undefined,
  ) {
    if (key !== illegalConstructorKey) {
      webidl.illegalConstructor();
    }

    super(name, "measure", startTime, duration, key);
    this[webidl.brand] = webidl.brand;
    this[_detail] = getStructuredClone()(detail);
  }

  toJSON() {
    webidl.assertBranded(this, PerformanceMeasurePrototype);
    return {
      name: this.name,
      entryType: this.entryType,
      startTime: this.startTime,
      duration: this.duration,
      detail: this.detail,
    };
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return inspect(
      getCreateFilteredInspectProxy()({
        object: this,
        evaluate: ObjectPrototypeIsPrototypeOf(
          PerformanceMeasurePrototype,
          this,
        ),
        keys: [
          "name",
          "entryType",
          "startTime",
          "duration",
          "detail",
        ],
      }),
      inspectOptions,
    );
  }
}
webidl.configureInterface(PerformanceMeasure);
const PerformanceMeasurePrototype = PerformanceMeasure.prototype;

function scheduleObserverCallback(observer) {
  if (observer[_scheduled]) {
    return;
  }
  observer[_scheduled] = true;
  queueMicrotask(() => {
    observer[_scheduled] = false;
    const entries = observer[_buffer];
    observer[_buffer] = [];
    if (entries.length > 0) {
      const entryList = new PerformanceObserverEntryList(
        entries,
        illegalConstructorKey,
      );
      observer[_callback](entryList, observer);
    }
  });
}

function queuePerformanceEntry(entry) {
  for (let i = 0; i < performanceObservers.length; i++) {
    const observer = performanceObservers[i];
    if (ArrayPrototypeIncludes(observer[_entryTypes], entry.entryType)) {
      ArrayPrototypePush(observer[_buffer], entry);
      scheduleObserverCallback(observer);
    }
  }
}

const _entries = Symbol("[[entries]]");

class PerformanceObserverEntryList {
  [_entries] = [];

  constructor(entries, key = undefined) {
    if (key !== illegalConstructorKey) {
      webidl.illegalConstructor();
    }
    this[webidl.brand] = webidl.brand;
    this[_entries] = entries;
  }

  getEntries() {
    webidl.assertBranded(this, PerformanceObserverEntryListPrototype);
    return ArrayPrototypeSlice(this[_entries]);
  }

  getEntriesByType(type) {
    webidl.assertBranded(this, PerformanceObserverEntryListPrototype);
    const prefix =
      "Failed to execute 'getEntriesByType' on 'PerformanceObserverEntryList'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    type = webidl.converters.DOMString(type, prefix, "Argument 1");
    return ArrayPrototypeFilter(
      this[_entries],
      (entry) => entry.entryType === type,
    );
  }

  getEntriesByName(name, type = undefined) {
    webidl.assertBranded(this, PerformanceObserverEntryListPrototype);
    const prefix =
      "Failed to execute 'getEntriesByName' on 'PerformanceObserverEntryList'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    name = webidl.converters.DOMString(name, prefix, "Argument 1");
    if (type !== undefined) {
      type = webidl.converters.DOMString(type, prefix, "Argument 2");
    }
    return ArrayPrototypeFilter(
      this[_entries],
      (entry) =>
        entry.name === name && (type === undefined || entry.entryType === type),
    );
  }
}
webidl.configureInterface(PerformanceObserverEntryList);
const PerformanceObserverEntryListPrototype =
  PerformanceObserverEntryList.prototype;

const _callback = Symbol("[[callback]]");
const _entryTypes = Symbol("[[entryTypes]]");
const _buffer = Symbol("[[buffer]]");
const _scheduled = Symbol("[[scheduled]]");

class PerformanceObserver {
  static get supportedEntryTypes() {
    return ["mark", "measure"];
  }

  [_callback] = null;
  [_entryTypes] = [];
  [_buffer] = [];
  [_scheduled] = false;

  constructor(callback) {
    const prefix = "Failed to construct 'PerformanceObserver'";
    webidl.requiredArguments(arguments.length, 1, prefix);
    if (typeof callback !== "function") {
      throw new TypeError(
        `${prefix}: The callback provided as parameter 1 is not a function.`,
      );
    }
    this[webidl.brand] = webidl.brand;
    this[_callback] = callback;
  }

  observe(options = { __proto__: null }) {
    webidl.assertBranded(this, PerformanceObserverPrototype);
    const prefix = "Failed to execute 'observe' on 'PerformanceObserver'";

    if (options === undefined || options === null) {
      throw new TypeError(
        `${prefix}: 1 argument required, but only 0 present.`,
      );
    }

    const { entryTypes, type } = options;

    if (entryTypes !== undefined && type !== undefined) {
      throw new TypeError(
        `${prefix}: Cannot specify both 'entryTypes' and 'type'.`,
      );
    }

    if (entryTypes === undefined && type === undefined) {
      throw new TypeError(
        `${prefix}: Either 'entryTypes' or 'type' must be specified.`,
      );
    }

    let types;
    if (entryTypes !== undefined) {
      if (!ArrayIsArray(entryTypes)) {
        throw new TypeError(`${prefix}: 'entryTypes' must be an array.`);
      }
      types = ArrayPrototypeFilter(
        entryTypes,
        (t) =>
          ArrayPrototypeIncludes(PerformanceObserver.supportedEntryTypes, t),
      );
      if (types.length === 0) {
        return;
      }
    } else {
      if (
        !ArrayPrototypeIncludes(PerformanceObserver.supportedEntryTypes, type)
      ) {
        return;
      }
      types = [type];
    }

    this[_entryTypes] = types;
    this[_buffer] = [];

    if (!ArrayPrototypeIncludes(performanceObservers, this)) {
      ArrayPrototypePush(performanceObservers, this);
    }

    // Per https://w3c.github.io/performance-timeline/#dom-performanceobserver-observe,
    // when the single-type form is used with buffered=true, prepend any
    // already-recorded entries of that type to the observer's buffer.
    if (type !== undefined && options.buffered === true) {
      let appended = false;
      for (let i = 0; i < performanceEntries.length; i++) {
        const entry = performanceEntries[i];
        if (entry.entryType === type) {
          ArrayPrototypePush(this[_buffer], entry);
          appended = true;
        }
      }
      if (appended) {
        scheduleObserverCallback(this);
      }
    }
  }

  disconnect() {
    webidl.assertBranded(this, PerformanceObserverPrototype);
    const index = ArrayPrototypeIndexOf(performanceObservers, this);
    if (index !== -1) {
      ArrayPrototypeSplice(performanceObservers, index, 1);
    }
    this[_entryTypes] = [];
    this[_buffer] = [];
  }

  takeRecords() {
    webidl.assertBranded(this, PerformanceObserverPrototype);
    const records = this[_buffer];
    this[_buffer] = [];
    return records;
  }
}
webidl.configureInterface(PerformanceObserver);
const PerformanceObserverPrototype = PerformanceObserver.prototype;

class Performance extends EventTarget {
  constructor(key = null) {
    if (key != illegalConstructorKey) {
      webidl.illegalConstructor();
    }

    super();
    this[webidl.brand] = webidl.brand;
  }

  get timeOrigin() {
    webidl.assertBranded(this, PerformancePrototype);
    return timeOrigin;
  }

  clearMarks(markName = undefined) {
    webidl.assertBranded(this, PerformancePrototype);
    if (markName !== undefined) {
      markName = webidl.converters.DOMString(
        markName,
        "Failed to execute 'clearMarks' on 'Performance'",
        "Argument 1",
      );

      performanceEntries = ArrayPrototypeFilter(
        performanceEntries,
        (entry) => !(entry.name === markName && entry.entryType === "mark"),
      );
    } else {
      performanceEntries = ArrayPrototypeFilter(
        performanceEntries,
        (entry) => entry.entryType !== "mark",
      );
    }
  }

  clearMeasures(measureName = undefined) {
    webidl.assertBranded(this, PerformancePrototype);
    if (measureName !== undefined) {
      measureName = webidl.converters.DOMString(
        measureName,
        "Failed to execute 'clearMeasures' on 'Performance'",
        "Argument 1",
      );

      performanceEntries = ArrayPrototypeFilter(
        performanceEntries,
        (entry) =>
          !(entry.name === measureName && entry.entryType === "measure"),
      );
    } else {
      performanceEntries = ArrayPrototypeFilter(
        performanceEntries,
        (entry) => entry.entryType !== "measure",
      );
    }
  }

  clearResourceTimings() {
    webidl.assertBranded(this, PerformancePrototype);
    performanceEntries = ArrayPrototypeFilter(
      performanceEntries,
      (entry) => entry.entryType !== "resource",
    );
  }

  setResourceTimingBufferSize(_maxSize) {
    webidl.assertBranded(this, PerformancePrototype);
    webidl.requiredArguments(
      arguments.length,
      1,
      "Failed to execute 'setResourceTimingBufferSize' on 'Performance'",
    );
    // This is a noop in Deno as we don't have resource timing entries
  }

  getEntries() {
    webidl.assertBranded(this, PerformancePrototype);
    return filterByNameType();
  }

  getEntriesByName(
    name,
    type = undefined,
  ) {
    webidl.assertBranded(this, PerformancePrototype);
    const prefix = "Failed to execute 'getEntriesByName' on 'Performance'";
    webidl.requiredArguments(arguments.length, 1, prefix);

    name = webidl.converters.DOMString(name, prefix, "Argument 1");

    if (type !== undefined) {
      type = webidl.converters.DOMString(type, prefix, "Argument 2");
    }

    return filterByNameType(name, type);
  }

  getEntriesByType(type) {
    webidl.assertBranded(this, PerformancePrototype);
    const prefix = "Failed to execute 'getEntriesByName' on 'Performance'";
    webidl.requiredArguments(arguments.length, 1, prefix);

    type = webidl.converters.DOMString(type, prefix, "Argument 1");

    return filterByNameType(undefined, type);
  }

  mark(
    markName,
    markOptions = { __proto__: null },
  ) {
    webidl.assertBranded(this, PerformancePrototype);
    const prefix = "Failed to execute 'mark' on 'Performance'";
    webidl.requiredArguments(arguments.length, 1, prefix);

    markName = webidl.converters.DOMString(markName, prefix, "Argument 1");

    markOptions = webidl.converters.PerformanceMarkOptions(
      markOptions,
      prefix,
      "Argument 2",
    );

    // 3.1.1.1 If the global object is a Window object and markName uses the
    // same name as a read only attribute in the PerformanceTiming interface,
    // throw a SyntaxError. - not implemented
    const entry = new PerformanceMark(markName, markOptions);
    ArrayPrototypePush(performanceEntries, entry);
    queuePerformanceEntry(entry);
    return entry;
  }

  measure(
    measureName,
    startOrMeasureOptions = { __proto__: null },
    endMark = undefined,
  ) {
    webidl.assertBranded(this, PerformancePrototype);
    const prefix = "Failed to execute 'measure' on 'Performance'";
    webidl.requiredArguments(arguments.length, 1, prefix);

    measureName = webidl.converters.DOMString(
      measureName,
      prefix,
      "Argument 1",
    );

    startOrMeasureOptions = webidl.converters
      ["DOMString or PerformanceMeasureOptions"](
        startOrMeasureOptions,
        prefix,
        "Argument 2",
      );

    if (endMark !== undefined) {
      endMark = webidl.converters.DOMString(endMark, prefix, "Argument 3");
    }

    if (
      startOrMeasureOptions && typeof startOrMeasureOptions === "object" &&
      ObjectKeys(startOrMeasureOptions).length > 0
    ) {
      if (endMark) {
        throw new TypeError('Options cannot be passed with "endMark"');
      }
      if (
        ReflectHas(startOrMeasureOptions, "start") &&
        ReflectHas(startOrMeasureOptions, "duration") &&
        ReflectHas(startOrMeasureOptions, "end")
      ) {
        throw new TypeError(
          'Cannot specify "start", "end", and "duration" together in options',
        );
      }
    }
    let endTime;
    if (endMark) {
      endTime = convertMarkToTimestamp(endMark);
    } else if (
      typeof startOrMeasureOptions === "object" &&
      ReflectHas(startOrMeasureOptions, "end")
    ) {
      endTime = convertMarkToTimestamp(startOrMeasureOptions.end);
    } else if (
      typeof startOrMeasureOptions === "object" &&
      ReflectHas(startOrMeasureOptions, "start") &&
      ReflectHas(startOrMeasureOptions, "duration")
    ) {
      const start = convertMarkToTimestamp(startOrMeasureOptions.start);
      const duration = convertMarkToTimestamp(startOrMeasureOptions.duration);
      endTime = start + duration;
    } else {
      endTime = now();
    }
    let startTime;
    if (
      typeof startOrMeasureOptions === "object" &&
      ReflectHas(startOrMeasureOptions, "start")
    ) {
      startTime = convertMarkToTimestamp(startOrMeasureOptions.start);
    } else if (
      typeof startOrMeasureOptions === "object" &&
      ReflectHas(startOrMeasureOptions, "end") &&
      ReflectHas(startOrMeasureOptions, "duration")
    ) {
      const end = convertMarkToTimestamp(startOrMeasureOptions.end);
      const duration = convertMarkToTimestamp(startOrMeasureOptions.duration);
      startTime = end - duration;
    } else if (typeof startOrMeasureOptions === "string") {
      startTime = convertMarkToTimestamp(startOrMeasureOptions);
    } else {
      startTime = 0;
    }
    const entry = new PerformanceMeasure(
      measureName,
      startTime,
      endTime - startTime,
      typeof startOrMeasureOptions === "object"
        ? startOrMeasureOptions.detail ?? null
        : null,
      illegalConstructorKey,
    );
    ArrayPrototypePush(performanceEntries, entry);
    queuePerformanceEntry(entry);
    return entry;
  }

  now() {
    webidl.assertBranded(this, PerformancePrototype);
    return now();
  }

  toJSON() {
    webidl.assertBranded(this, PerformancePrototype);
    return {
      timeOrigin: this.timeOrigin,
    };
  }

  [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
    return inspect(
      getCreateFilteredInspectProxy()({
        object: this,
        evaluate: ObjectPrototypeIsPrototypeOf(PerformancePrototype, this),
        keys: ["timeOrigin"],
      }),
      inspectOptions,
    );
  }
}
webidl.configureInterface(Performance);
const PerformancePrototype = Performance.prototype;

webidl.converters["Performance"] = webidl.createInterfaceConverter(
  "Performance",
  PerformancePrototype,
);

const performance = new Performance(illegalConstructorKey);

return {
  Performance,
  performance,
  PerformanceEntry,
  PerformanceMark,
  PerformanceMeasure,
  PerformanceObserver,
  PerformanceObserverEntryList,
  setTimeOrigin,
};
})();
