// Copyright 2018-2026 the Deno authors. MIT license.
// Copyright Joyent and Node contributors. All rights reserved. MIT license.

// deno-lint-ignore-file no-this-alias

(function () {
const { core, primordials } = __bootstrap;
const {
  createTimer: createTimer_,
  cancelTimer: cancelTimer_,
  refreshTimer: refreshTimer_,
  refTimer: refTimer_,
  unrefTimer: unrefTimer_,
  getAsyncContext,
  setAsyncContext,
  immediateRefCount,
} = core;
const {
  ArrayPrototypePush,
  DateNow,
  FunctionPrototypeCall,
  MapPrototypeDelete,
  MapPrototypeGet,
  MapPrototypeSet,
  NumberIsFinite,
  NumberIsNaN,
  ObjectDefineProperty,
  ReflectApply,
  SafeMapIterator,
  SafeMap,
  Symbol,
  SymbolDispose,
  SymbolToPrimitive,
} = primordials;
const {
  emitAfter,
  emitBefore,
  emitDestroy,
  emitInit,
  enabledHooksExist,
  executionAsyncId,
  newAsyncId: nextAsyncId,
} = core.loadExtScript("ext:deno_node/internal/async_hooks.ts");
const { inspect } = core.loadExtScript(
  "ext:deno_node/internal/util/inspect.mjs",
);
const {
  validateFunction,
  validateNumber,
} = core.loadExtScript("ext:deno_node/internal/validators.mjs");
const { ERR_OUT_OF_RANGE } = core.loadExtScript(
  "ext:deno_node/internal/errors.ts",
);
const lazyProcess = core.createLazyLoader("node:process");

// Timeout values > TIMEOUT_MAX are set to 1.
const TIMEOUT_MAX = 2 ** 31 - 1;

const kDestroy = Symbol("destroy");
const kTimerId = Symbol("timerId");
const kSystem = Symbol("system");
const kTimeout = Symbol("timeout");
const kSuspended = Symbol("suspended");
const kRefed = core.kRefed;
const createTimer = Symbol("createTimer");

/**
 * The keys in this map correspond to the key ID's in the spec's map of active
 * timers. The values are the timeout's status.
 *
 * @type {Map<number, Timeout>}
 */
const activeTimers = new SafeMap();

/**
 * @param {number} id
 * @returns {Timeout | undefined}
 */
function getActiveTimer(id) {
  return MapPrototypeGet(activeTimers, id);
}

function getActiveResourcesInfo() {
  const resources = [];
  for (const { 1: timeout } of new SafeMapIterator(activeTimers)) {
    if (timeout[kRefed]) {
      ArrayPrototypePush(resources, "Timeout");
    }
  }
  // Immediates aren't tracked in a JS-side collection: they're queued into and
  // consumed by core, which keeps the count of refed (loop-alive) immediates.
  const immediateCount = core.getActiveImmediateCount();
  for (let i = 0; i < immediateCount; i++) {
    ArrayPrototypePush(resources, "Immediate");
  }
  return resources;
}

let warnedNegativeNumber = false;
let warnedNotNumber = false;

// Timer constructor function.
// isSystem marks the timer as runtime-internal (e.g. setUnrefTimeout's
// keep-alive timers); leak/sanitizer machinery skips these so an
// internal timer doesn't surface as a user-visible leak.
function Timeout(callback, after, args, isRepeat, isRefed, isSystem) {
  if (after === undefined) {
    after = 1;
  } else {
    after *= 1; // Coalesce to number or NaN
  }

  if (!(after >= 1 && after <= TIMEOUT_MAX)) {
    if (after > TIMEOUT_MAX) {
      lazyProcess().default.emitWarning(
        `${after} does not fit into a 32-bit signed integer.` +
          "\nTimeout duration was set to 1.",
        "TimeoutOverflowWarning",
      );
    } else if (after < 0 && !warnedNegativeNumber) {
      warnedNegativeNumber = true;
      lazyProcess().default.emitWarning(
        `${after} is a negative number.` +
          "\nTimeout duration was set to 1.",
        "TimeoutNegativeWarning",
      );
    } else if (NumberIsNaN(after) && !warnedNotNumber) {
      warnedNotNumber = true;
      lazyProcess().default.emitWarning(
        `${after} is not a number.` +
          "\nTimeout duration was set to 1.",
        "TimeoutNaNWarning",
      );
    }
    after = 1;
  }
  this._idleTimeout = after;
  this._idleStart = DateNow();
  this._idlePrev = null;
  this._idleNext = null;
  this._onTimeout = callback;
  this._timerArgs = args;
  this._repeat = isRepeat;
  this._destroyed = false;
  ObjectDefineProperty(this, kSuspended, {
    __proto__: null,
    value: false,
    writable: true,
  });
  this[kRefed] = isRefed;
  // Non-enumerable: this is a runtime-internal marker and must not leak into
  // `util.inspect(timer)` output (node has no such property).
  ObjectDefineProperty(this, kSystem, {
    __proto__: null,
    value: !!isSystem,
    writable: true,
  });

  const asyncId = nextAsyncId();
  const triggerAsyncId = executionAsyncId();
  this._asyncId = asyncId;
  this._triggerAsyncId = triggerAsyncId;
  this._asyncDestroyed = false;

  this[kTimerId] = this[createTimer]();

  // Match node: only emit async_hooks init if there are live hooks.
  // emitInit does non-trivial work (try/finally, empty-array loop,
  // lookupPublicResource) that's pure overhead in the common case.
  if (enabledHooksExist()) {
    emitInit(asyncId, "Timeout", triggerAsyncId, this);
  }
}

Timeout.prototype[createTimer] = function () {
  const self = this;
  const callback = this._onTimeout;
  const asyncContext = getAsyncContext();
  const asyncId = this._asyncId;
  const triggerAsyncId = this._triggerAsyncId;
  // Fast path: when no async_hooks are registered, the emit* calls are
  // pure overhead (array push/pop + empty-array iteration). Keep the
  // outer closure (ALS context must still propagate) but elide the
  // hook machinery.
  let cb;
  function invokeCallback() {
    const wasRepeat = self._repeat;
    if (wasRepeat) {
      const currentCb = self._onTimeout;
      if (currentCb === null) {
        self[kDestroy]();
        return;
      }
    }
    const currentCb = wasRepeat ? self._onTimeout : callback;
    const args = self._timerArgs;
    try {
      if (args !== undefined && args.length > 0) {
        return ReflectApply(currentCb, self, args);
      } else {
        return FunctionPrototypeCall(currentCb, self);
      }
    } finally {
      // Cleanup runs after the callback (in a `finally` so it happens even if
      // the callback throws). Node keeps a one-shot Timeout reported as an
      // active resource during its own callback and only drops it afterwards,
      // so `activeTimers` must retain it for the duration of the call.
      if (wasRepeat) {
        if (self._idleTimeout < 0 || self._onTimeout === null) {
          self[kDestroy]();
        }
      } else if (self._repeat) {
        // timeout was converted to interval inside callback
        MapPrototypeDelete(activeTimers, self[kTimerId]);
        self[kTimerId] = self[createTimer]();
      } else {
        MapPrototypeDelete(activeTimers, self[kTimerId]);
        self._destroyed = true;
      }
    }
  }
  if (enabledHooksExist()) {
    cb = function () {
      const oldContext = getAsyncContext();
      try {
        setAsyncContext(asyncContext);
        emitBefore(asyncId, triggerAsyncId, self);
        const ret = invokeCallback();
        // Only emit after/destroy on success. On error, the domain's
        // uncaught exception handler manages the stack cleanup.
        emitAfter(asyncId);
        if (!self._repeat && !self._asyncDestroyed) {
          self._asyncDestroyed = true;
          emitDestroy(asyncId);
        }
        return ret;
      } finally {
        setAsyncContext(oldContext);
      }
    };
  } else {
    cb = function () {
      const oldContext = getAsyncContext();
      try {
        setAsyncContext(asyncContext);
        return invokeCallback();
      } finally {
        setAsyncContext(oldContext);
      }
    };
  }
  const timer = createTimer_(
    cb,
    this._idleTimeout,
    undefined,
    this._repeat,
    this[kRefed],
    this[kSystem],
  );
  ObjectDefineProperty(this, "_timer", {
    __proto__: null,
    value: timer,
    writable: true,
    enumerable: false,
    configurable: true,
  });
  const id = timer._timerId;
  MapPrototypeSet(activeTimers, id, this);
  return id;
};

Timeout.prototype[kDestroy] = function () {
  if (!this._destroyed || this[kSuspended]) {
    this._destroyed = true;
    this[kSuspended] = false;
    this._idleTimeout = -1;
    this._idleStart = DateNow();
    this._onTimeout = null;
    cancelTimer_(this._timer);
    MapPrototypeDelete(activeTimers, this[kTimerId]);
    if (
      this._asyncId !== undefined &&
      !this._asyncDestroyed &&
      enabledHooksExist()
    ) {
      this._asyncDestroyed = true;
      emitDestroy(this._asyncId);
    }
  }
};

// Make sure the linked list only shows the minimal necessary information.
Timeout.prototype[inspect.custom] = function (_, options) {
  return inspect(this, {
    ...options,
    // Only inspect one level.
    depth: 0,
    // It should not recurse.
    customInspect: false,
  });
};

Timeout.prototype.refresh = function () {
  if (this._destroyed) {
    // Reactivate a timer that fired naturally (callback still set).
    // Do NOT reactivate a timer cancelled via clearTimeout (callback
    // nulled by kDestroy or _onTimeout explicitly cleared).
    if (this._onTimeout !== null) {
      this._destroyed = false;
      this[kSuspended] = false;
      this[kTimerId] = this[createTimer]();
    }
  } else {
    refreshTimer_(this._timer);
  }
  this._idleStart = DateNow();
  return this;
};

Timeout.prototype.unref = function () {
  if (this[kRefed]) {
    this[kRefed] = false;
    if (!this._destroyed) {
      unrefTimer_(this._timer);
    }
  }
  return this;
};

Timeout.prototype.ref = function () {
  if (!this[kRefed]) {
    this[kRefed] = true;
    if (!this._destroyed) {
      refTimer_(this._timer);
    }
  }
  return this;
};

Timeout.prototype.close = function () {
  this[kDestroy]();
  return this;
};

Timeout.prototype[SymbolDispose] = function () {
  this[kDestroy]();
};

Timeout.prototype.hasRef = function () {
  return this[kRefed];
};

Timeout.prototype[SymbolToPrimitive] = function () {
  return this[kTimerId];
};

/**
 * @param {number} msecs
 * @param {string} name
 * @returns
 */
function getTimerDuration(msecs, name) {
  validateNumber(msecs, name);

  if (msecs < 0 || !NumberIsFinite(msecs)) {
    throw new ERR_OUT_OF_RANGE(name, "a non-negative finite number", msecs);
  }

  // Ensure that msecs fits into signed int32
  if (msecs > TIMEOUT_MAX) {
    lazyProcess().default.emitWarning(
      `${msecs} does not fit into a 32-bit signed integer.` +
        `\nTimer duration was truncated to ${TIMEOUT_MAX}.`,
      "TimeoutOverflowWarning",
    );

    return TIMEOUT_MAX;
  }

  return msecs;
}

function setUnrefTimeout(callback, timeout, ...args) {
  validateFunction(callback, "callback");
  // isSystem=true: this is a runtime-internal timer (keep-alive, socket
  // timeouts, idle eviction etc.). The leak sanitizer skips system timers
  // so e.g. Agent.keepSocketAlive's 5000 ms unref'd timer stays invisible
  // to Deno.test's `sanitizeOps`.
  return new Timeout(callback, timeout, args, false, false, true);
}

function suspendTimeout(timeout) {
  if (timeout !== null && timeout !== undefined && !timeout._destroyed) {
    timeout._destroyed = true;
    timeout[kSuspended] = true;
    timeout._idleStart = DateNow();
    cancelTimer_(timeout._timer);
    MapPrototypeDelete(activeTimers, timeout[kTimerId]);
  }
}

// Re-export immediate queue and runImmediates from core for consumers
const immediateQueue = core.immediateQueue;
const runImmediates = core.runImmediates;

class Immediate {
  constructor(unboundCallback, ...args) {
    const asyncContext = getAsyncContext();
    // Match Node's `immediate._onImmediate(...argv)` invocation: the callback's
    // `this` is the Immediate instance, not the global.
    const self = this;
    const callback = (...argv) => {
      const oldContext = getAsyncContext();
      try {
        setAsyncContext(asyncContext);
        return ReflectApply(unboundCallback, self, argv);
      } finally {
        setAsyncContext(oldContext);
      }
    };

    this._idleNext = null;
    this._idlePrev = null;
    this._onImmediate = callback;
    this._argv = args;
    this._destroyed = false;
    this[kRefed] = false;

    const asyncId = nextAsyncId();
    const triggerAsyncId = executionAsyncId();
    this.asyncId = asyncId;
    this.triggerAsyncId = triggerAsyncId;
    emitInit(asyncId, "Immediate", triggerAsyncId, this);

    this.ref();
    core.queueImmediate(this);
  }

  ref() {
    if (this[kRefed] === false) {
      this[kRefed] = true;
      immediateRefCount(true);
    }
    return this;
  }

  unref() {
    if (this[kRefed] === true) {
      this[kRefed] = false;
      immediateRefCount(false);
    }
    return this;
  }

  hasRef() {
    return !!this[kRefed];
  }

  [SymbolDispose]() {
    core.clearImmediate(this);
  }

  [inspect.custom] = function (_, options) {
    return inspect(this, {
      ...options,
      // Only inspect one level.
      depth: 0,
      // It should not recurse.
      customInspect: false,
    });
  };
}

return {
  TIMEOUT_MAX,
  kDestroy,
  kTimerId,
  kTimeout,
  kRefed,
  getActiveTimer,
  getActiveResourcesInfo,
  Timeout,
  getTimerDuration,
  setUnrefTimeout,
  suspendTimeout,
  immediateQueue,
  runImmediates,
  Immediate,
};
})();
