oline } = require('internal/async_hooks');

// TODO(addaleax): Use a non-internal solution for this.
const kWeak = Symbol('kWeak');
const { WeakReference } = internalBinding('util');

// Overwrite process.domain with a getter/setter that will allow for more
// effective optimizations
const _domain = [null];
ObjectDefineProperty(process, 'domain', {
  enumerable: true,
  get: function() {
    return _domain[0];
  },
  set: function(arg) {
    return _domain[0] = arg;
  }
});

const pairing = new SafeMap();
const asyncHook = createHook({
  init(asyncId, type, triggerAsyncId, resource) {
    if (process.domain !== null && process.domain !== undefined) {
      // If this operation is created while in a domain, let's mark it
      pairing.set(asyncId, process.domain[kWeak]);
      // Promises from other contexts, such as with the VM module, should not
      // have a domain property as it can be used to escape the sandbox.
      if (type !== 'PROMISE' || resource instanceof Promise) {
        ObjectDefineProperty(resource, 'domain', {
          configurable: true,
          enumerable: false,
          value: process.domain,
          writable: true
        });
      }
    }
  },
  before(asyncId) {
    const current = pairing.get(asyncId);
    if (current !== undefined) { // Enter domain for this cb
      // We will get the domain through current.get(), because the resource
      // object's .domain property makes sure it is not garbage collected.
      // However, we do need to make the reference to the domain non-weak,
      // so that it cannot be garbage collected before the after() hook.
      current.incRef();
      current.get().enter();
    }
  },
  after(asyncId) {
    const current = pairing.get(asyncId);
    if (current !== undefined) { // Exit domain for this cb
      const domain = current.get();
      current.decRef();
      domain.exit();
    }
  },
  destroy(asyncId) {
    pairing.delete(asyncId); // cleaning up
  }
});

// When domains are in use, they claim full ownership of the
// uncaught exception capture callback.
if (process.hasUncaughtExceptionCaptureCallback()) {
  throw new ERR_DOMAIN_CALLBACK_NOT_AVAILABLE();
}

// Get the stack trace at the point where `domain` was required.
// eslint-disable-next-line no-restricted-syntax
const domainRequireStack = new Error('require(`domain`) at this point').stack;

const { setUncaughtExceptionCaptureCallback } = process;
process.setUncaughtExceptionCaptureCallback = function(fn) {
  const err = new ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE();
  err.stack = err.stack + '\n' + '-'.repeat(40) + '\n' + domainRequireStack;
  throw err;
};


let sendMakeCallbackDeprecation = false;
function emitMakeCallbackDeprecation({ target, method }) {
  if (!sendMakeCallbackDeprecation) {
    process.emitWarning(
      'Using a domain property in MakeCallback is deprecated. Use the ' +
      'async_context variant of MakeCallback or the AsyncResource class ' +
      'instead. ' +
      `(Triggered by calling ${method?.name || '<anonymous>'} ` +
      `on ${target?.constructor?.name}.)`,
      'DeprecationWarning', 'DEP0097');
    sendMakeCallbackDeprecation = true;
  }
}

function topLevelDomainCallback(cb, ...args) {
  const domain = this.domain;
  if (exports.active && domain)
    emitMakeCallbackDeprecation({ target: this, method: cb });

  if (domain)
    domain.enter();
  const ret = ReflectApply(cb, this, args);
  if (domain)
    domain.exit();

  return ret;
}

// It's possible to enter one domain while already inside
// another one. The stack is each entered domain.
let stack = [];
exports._stack = stack;
useDomainTrampoline(topLevelDomainCallback);

function updateExceptionCapture() {
  if (ArrayPrototypeEvery(stack,
                          (domain) => domain.listenerCount('error') === 0)) {
    setUncaughtExceptionCaptureCallback(null);
  } else {
    setUncaughtExceptionCaptureCallback(null);
    setUncaughtExceptionCaptureCallback((er) => {
      return process.domain._errorHandler(er);
    });
  }
}


process.on('newListener', (name, listener) => {
  if (name === 'uncaughtException' &&
      listener !== domainUncaughtExceptionClear) {
    // Make sure the first listener for `uncaughtException` always clears
    // the domain stack.
    process.removeListener(name, domainUncaughtExceptionClear);
    process.prependListener(name, domainUncaughtExceptionClear);
  }
});

process.on('removeListener', (name, listener) => {
  if (name === 'uncaughtException' &&
      listener !== domainUncaughtExceptionClear) {
    // If the domain listener would be the only remaining one, remove it.
    const listeners = process.listeners('uncaughtException');
    if (listeners.length === 1 && listeners[0] === domainUncaughtExceptionClear)
      process.removeListener(name, domainUncaughtExceptionClear);
  }
});

function domainUncaughtExceptionClear() {
  stack.length = 0;
  exports.active = process.domain = null;
  updateExceptionCapture();
}


class Domain extends EventEmitter {
  constructor() {
    super();

    this.members = [];
    this[kWeak] = new WeakReference(this);
    asyncHook.enable();

    this.on('removeListener', updateExceptionCapture);
    this.on('newListener', updateExceptionCapture);
  }
}

exports.Domain = Domain;

exports.create = exports.createDomain = function createDomain() {
  return new Domain();
};

// The active domain is always the one that we're currently in.
exports.active = null;
Domain.prototype.members = undefined;

// Called by process._fatalException in case an error was thrown.
Domain.prototype._errorHandler = function(er) {
  let caught = false;

  if ((typeof er === 'object' && er !== null) || typeof er === 'function') {
    ObjectDefineProperty(er, 'domain', {
      configurable: true,
      enumerable: false,
      value: this,
      writable: true
    });
    er.domainThrown = true;
  }
  // Pop all adjacent duplicates of the currently active domain from the stack.
  // This is done to prevent a domain's error handler to run within the context
  // of itself, and re-entering itself recursively handler as a result of an
  // exception thrown in its context.
  while (exports.active === this) {
    this.exit();
  }

  // The top-level domain-handler is handled separately.
  //
  // The reason is that if V8 was passed a command line option
  // asking it to abort on an uncaught exception (currently
  // "--abort-on-uncaught-exception"), we want an uncaught exception
  // in the top-level domain error handler to make the
  // process abort. Using try/catch here would always make V8 think
  // that these exceptions are caught, and thus would prevent it from
  // aborting in these cases.
  if (stack.length === 0) {
    // If there's no error handler, do not emit an 'error' event
    // as this would throw an error, make the process exit, and thus
    // prevent the process 'uncaughtException' event from being emitted
    // if a listener is set.
    if (EventEmitter.listenerCount(this, 'error') > 0) {
      // Clear the uncaughtExceptionCaptureCallback so that we know that, since
      // the top-level domain is not active anymore, it would be ok to abort on
      // an uncaught exception at this point
      setUncaughtExceptionCaptureCallback(null);
      try {
        caught = this.emit('error', er);
      } finally {
        updateExceptionCapture();
      }
    }
  } else {
    // Wrap this in a try/catch so we don't get infinite throwing
    try {
      // One of three things will happen here.
      //
      // 1. There is a handler, caught = true
      // 2. There is no handler, caught = false
      // 3. It throws, caught = false
      //
      // If caught is false after this, then there's no need to exit()
      // the domain, because we're going to crash the process anyway.
      caught = this.emit('error', er);
    } catch (er2) {
      // The domain error handler threw!  oh no!
      // See if another domain can catch THIS error,
      // or else crash on the original one.
      updateExceptionCapture();
      if (stack.length) {
        exports.active = process.domain = stack[stack.length - 1];
        caught = process.domain._errorHandler(er2);
      } else {
        // Pass on to the next exception handler.
        throw er2;
      }
    }
  }

  // Exit all domains on the stack.  Uncaught exceptions end the
  // current tick and no domains should be left on the stack
  // between ticks.
  domainUncaughtExceptionClear();

  return caught;
};


Domain.prototype.enter = function() {
  // Note that this might be a no-op, but we still need
  // to push it onto the stack so that we can pop it later.
  exports.active = process.domain = this;
  ArrayPrototypePush(stack, this);
  updateExceptionCapture();
};


Domain.prototype.exit = function() {
  // Don't do anything if this domain is not on the stack.
  const index = ArrayPrototypeLastIndexOf(stack, this);
  if (index === -1) return;

  // Exit all domains until this one.
  ArrayPrototypeSplice(stack, index);

  exports.active = stack.length === 0 ? undefined : stack[stack.length - 1];
  process.domain = exports.active;
  updateExceptionCapture();
};


// note: this works for timers as well.
Domain.prototype.add = function(ee) {
  // If the domain is already added, then nothing left to do.
  if (ee.domain === this)
    return;

  // Has a domain already - remove it first.
  if (ee.domain)
    ee.domain.remove(ee);

  // Check for circular Domain->Domain links.
  // They cause big issues.
  //
  // For example:
  // var d = domain.create();
  // var e = domain.create();
  // d.add(e);
  // e.add(d);
  // e.emit('error', er); // RangeError, stack overflow!
  if (this.domain && (ee instanceof Domain)) {
    for (let d = this.domain; d; d = d.domain) {
      if (ee === d) return;
    }
  }

  ObjectDefineProperty(ee, 'domain', {
    configurable: true,
    enumerable: false,
    value: this,
    writable: true
  });
  ArrayPrototypePush(this.members, ee);
};


Domain.prototype.remove = function(ee) {
  ee.domain = null;
  const index = ArrayPrototypeIndexOf(this.members, ee);
  if (index !== -1)
    ArrayPrototypeSplice(this.members, index, 1);
};


Domain.prototype.run = function(fn) {
  this.enter();
  const ret = ReflectApply(fn, this, ArrayPrototypeSlice(arguments, 1));
  this.exit();

  return ret;
};


function intercepted(_this, self, cb, fnargs) {
  if (fnargs[0] && fnargs[0] instanceof Error) {
    const er = fnargs[0];
    er.domainBound = cb;
    er.domainThrown = false;
    ObjectDefineProperty(er, 'domain', {
      configurable: true,
      enumerable: false,
      value: self,
      writable: true
    });
    self.emit('error', er);
    return;
  }

  self.enter();
  const ret = ReflectApply(cb, _this, ArrayPrototypeSlice(fnargs, 1));
  self.exit();

  return ret;
}


Domain.prototype.intercept = function(cb) {
  const self = this;

  function runIntercepted() {
    return intercepted(this, self, cb, arguments);
  }

  return runIntercepted;
};


function bound(_this, self, cb, fnargs) {
  self.enter();
  const ret = ReflectApply(cb, _this, fnargs);
  self.exit();

  return ret;
}


Domain.prototype.bind = function(cb) {
  const self = this;

  function runBound() {
    return bound(this, self, cb, arguments);
  }

  ObjectDefineProperty(runBound, 'domain', {
    configurable: true,
    enumerable: false,
    value: this,
    writable: true
  });

  return runBound;
};

// Override EventEmitter methods to make it domain-aware.
EventEmitter.usingDomains = true;

const eventInit = EventEmitter.init;
EventEmitter.init = function(opts) {
  ObjectDefineProperty(this, 'domain', {
    configurable: true,
    enumerable: false,
    value: null,
    writable: true
  });
  if (exports.active && !(this instanceof exports.Domain)) {
    this.domain = exports.active;
  }

  return FunctionPrototypeCall(eventInit, this, opts);
};

const eventEmit = EventEmitter.prototype.emit;
EventEmitter.prototype.emit = function emit(...args) {
  const domain = this.domain;

  const type = args[0];
  const shouldEmitError = type === 'error' &&
                          this.listenerCount(type) > 0;

  // Just call original `emit` if current EE instance has `error`
  // handler, there's no active domain or this is process
  if (shouldEmitError || domain === null || domain === undefined ||
      this === process) {
    return ReflectApply(eventEmit, this, args);
  }

  if (type === 'error') {
    const er = args.length > 1 && args[1] ?
      args[1] : new ERR_UNHANDLED_ERROR();

    if (typeof er === 'object') {
      er.domainEmitter = this;
      ObjectDefineProperty(er, 'domain', {
        configurable: true,
        enumerable: false,
        value: domain,
        writable: true
      });
      er.domainThrown = false;
    }

    // Remove the current domain (and its duplicates) from the domains stack and
    // set the active domain to its parent (if any) so that the domain's error
    // handler doesn't run in its own context. This prevents any event emitter
    // created or any exception thrown in that error handler from recursively
    // executing that error handler.
    const origDomainsStack = ArrayPrototypeSlice(stack);
    const origActiveDomain = process.domain;

    // Travel the domains stack from top to bottom to find the first domain
    // instance that is not a duplicate of the current active domain.
    let idx = stack.length - 1;
    while (idx > -1 && process.domain === stack[idx]) {
      --idx;
    }

    // Change the stack to not contain the current active domain, and only the
    // domains above it on the stack.
    if (idx < 0) {
      stack.length = 0;
    } else {
      ArrayPrototypeSplice(stack, idx + 1);
    }

    // Change the current active domain
    if (stack.length > 0) {
      exports.active = process.domain = stack[stack.length - 1];
    } else {
      exports.active = process.domain = null;
    }

    updateExceptionCapture();

    domain.emit('error', er);

    // Now that the domain's error handler has completed, restore the domains
    // stack and the active domain to their original values.
    exports._stack = stack = origDomainsStack;
    exports.active = process.domain = origActiveDomain;
    updateExceptionCapture();

    return false;
  }

  domain.enter();
  const ret = ReflectApply(eventEmit, this, args);
  domain.exit();

  return ret;
};
