'use strict';

const async_wrap = process.binding('async_wrap');
const errors = require('internal/errors');
/* async_hook_fields is a Uint32Array wrapping the uint32_t array of
 * Environment::AsyncHooks::fields_[]. Each index tracks the number of active
 * hooks for each type.
 *
 * async_id_fields is a Float64Array wrapping the double array of
 * Environment::AsyncHooks::async_id_fields_[]. Each index contains the ids for
 * the various asynchronous states of the application. These are:
 *  kExecutionAsyncId: The async_id assigned to the resource responsible for the
 *    current execution stack.
 *  kTriggerAsyncId: The trigger_async_id of the resource responsible for
 *    the current execution stack.
 *  kAsyncIdCounter: Incremental counter tracking the next assigned async_id.
 *  kInitTriggerAsyncId: Written immediately before a resource's constructor
 *    that sets the value of the init()'s triggerAsyncId. The order of
 *    retrieving the triggerAsyncId value is passing directly to the
 *    constructor -> value set in kInitTriggerAsyncId -> executionAsyncId of
 *    the current resource.
 */
const { async_hook_fields, async_id_fields } = async_wrap;
// Store the pair executionAsyncId and triggerAsyncId in a std::stack on
// Environment::AsyncHooks::ids_stack_ tracks the resource responsible for the
// current execution stack. This is unwound as each resource exits. In the case
// of a fatal exception this stack is emptied after calling each hook's after()
// callback.
const { pushAsyncIds, popAsyncIds } = async_wrap;
// For performance reasons, only track Proimses when a hook is enabled.
const { enablePromiseHook, disablePromiseHook } = async_wrap;
// Properties in active_hooks are used to keep track of the set of hooks being
// executed in case another hook is enabled/disabled. The new set of hooks is
// then restored once the active set of hooks is finished executing.
const active_hooks = {
  // Array of all AsyncHooks that will be iterated whenever an async event
  // fires. Using var instead of (preferably const) in order to assign
  // active_hooks.tmp_array if a hook is enabled/disabled during hook
  // execution.
  array: [],
  // Use a counter to track nested calls of async hook callbacks and make sure
  // the active_hooks.array isn't altered mid execution.
  call_depth: 0,
  // Use to temporarily store and updated active_hooks.array if the user
  // enables or disables a hook while hooks are being processed. If a hook is
  // enabled() or disabled() during hook execution then the current set of
  // active hooks is duplicated and set equal to active_hooks.tmp_array. Any
  // subsequent changes are on the duplicated array. When all hooks have
  // completed executing active_hooks.tmp_array is assigned to
  // active_hooks.array.
  tmp_array: null,
  // Keep track of the field counts held in active_hooks.tmp_array. Because the
  // async_hook_fields can't be reassigned, store each uint32 in an array that
  // is written back to async_hook_fields when active_hooks.array is restored.
  tmp_fields: null
};


// Each constant tracks how many callbacks there are for any given step of
// async execution. These are tracked so if the user didn't include callbacks
// for a given step, that step can bail out early.
const { kInit, kBefore, kAfter, kDestroy, kTotals, kPromiseResolve,
        kCheck, kExecutionAsyncId, kTriggerAsyncId, kAsyncIdCounter,
        kInitTriggerAsyncId } = async_wrap.constants;

// Symbols used to store the respective ids on both AsyncResource instances and
// internal resources. They will also be assigned to arbitrary objects passed
// in by the user that take place of internally constructed objects.
const { async_id_symbol, trigger_async_id_symbol } = async_wrap;

// Used in AsyncHook and AsyncResource.
const init_symbol = Symbol('init');
const before_symbol = Symbol('before');
const after_symbol = Symbol('after');
const destroy_symbol = Symbol('destroy');
const promise_resolve_symbol = Symbol('promiseResolve');
const emitBeforeNative = emitHookFactory(before_symbol, 'emitBeforeNative');
const emitAfterNative = emitHookFactory(after_symbol, 'emitAfterNative');
const emitDestroyNative = emitHookFactory(destroy_symbol, 'emitDestroyNative');
const emitPromiseResolveNative =
    emitHookFactory(promise_resolve_symbol, 'emitPromiseResolveNative');

// TODO(refack): move to node-config.cc
const abort_regex = /^--abort[_-]on[_-]uncaught[_-]exception$/;

// Setup the callbacks that node::AsyncWrap will call when there are hooks to
// process. They use the same functions as the JS embedder API. These callbacks
// are setup immediately to prevent async_wrap.setupHooks() from being hijacked
// and the cost of doing so is negligible.
async_wrap.setupHooks({ init: emitInitNative,
                        before: emitBeforeNative,
                        after: emitAfterNative,
                        destroy: emitDestroyNative,
                        promise_resolve: emitPromiseResolveNative });

// Used to fatally abort the process if a callback throws.
function fatalError(e) {
  if (typeof e.stack === 'string') {
    process._rawDebug(e.stack);
  } else {
    const o = { message: e };
    Error.captureStackTrace(o, fatalError);
    process._rawDebug(o.stack);
  }
  if (process.execArgv.some((e) => abort_regex.test(e))) {
    process.abort();
  }
  process.exit(1);
}


// Public API //

class AsyncHook {
  constructor({ init, before, after, destroy, promiseResolve }) {
    if (init !== undefined && typeof init !== 'function')
      throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'init');
    if (before !== undefined && typeof before !== 'function')
      throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'before');
    if (after !== undefined && typeof after !== 'function')
      throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'before');
    if (destroy !== undefined && typeof destroy !== 'function')
      throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'before');
    if (promiseResolve !== undefined && typeof promiseResolve !== 'function')
      throw new errors.TypeError('ERR_ASYNC_CALLBACK', 'promiseResolve');

    this[init_symbol] = init;
    this[before_symbol] = before;
    this[after_symbol] = after;
    this[destroy_symbol] = destroy;
    this[promise_resolve_symbol] = promiseResolve;
  }

  enable() {
    // The set of callbacks for a hook should be the same regardless of whether
    // enable()/disable() are run during their execution. The following
    // references are reassigned to the tmp arrays if a hook is currently being
    // processed.
    const [hooks_array, hook_fields] = getHookArrays();

    // Each hook is only allowed to be added once.
    if (hooks_array.includes(this))
      return this;

    const prev_kTotals = hook_fields[kTotals];
    hook_fields[kTotals] = 0;

    // createHook() has already enforced that the callbacks are all functions,
    // so here simply increment the count of whether each callbacks exists or
    // not.
    hook_fields[kTotals] += hook_fields[kInit] += +!!this[init_symbol];
    hook_fields[kTotals] += hook_fields[kBefore] += +!!this[before_symbol];
    hook_fields[kTotals] += hook_fields[kAfter] += +!!this[after_symbol];
    hook_fields[kTotals] += hook_fields[kDestroy] += +!!this[destroy_symbol];
    hook_fields[kTotals] +=
        hook_fields[kPromiseResolve] += +!!this[promise_resolve_symbol];
    hooks_array.push(this);

    if (prev_kTotals === 0 && hook_fields[kTotals] > 0) {
      enablePromiseHook();
      hook_fields[kCheck] += 1;
    }

    return this;
  }

  disable() {
    const [hooks_array, hook_fields] = getHookArrays();

    const index = hooks_array.indexOf(this);
    if (index === -1)
      return this;

    const prev_kTotals = hook_fields[kTotals];
    hook_fields[kTotals] = 0;

    hook_fields[kTotals] += hook_fields[kInit] -= +!!this[init_symbol];
    hook_fields[kTotals] += hook_fields[kBefore] -= +!!this[before_symbol];
    hook_fields[kTotals] += hook_fields[kAfter] -= +!!this[after_symbol];
    hook_fields[kTotals] += hook_fields[kDestroy] -= +!!this[destroy_symbol];
    hook_fields[kTotals] +=
        hook_fields[kPromiseResolve] -= +!!this[promise_resolve_symbol];
    hooks_array.splice(index, 1);

    if (prev_kTotals > 0 && hook_fields[kTotals] === 0) {
      disablePromiseHook();
      hook_fields[kCheck] -= 1;
    }

    return this;
  }
}


function getHookArrays() {
  if (active_hooks.call_depth === 0)
    return [active_hooks.array, async_hook_fields];
  // If this hook is being enabled while in the middle of processing the array
  // of currently active hooks then duplicate the current set of active hooks
  // and store this there. This shouldn't fire until the next time hooks are
  // processed.
  if (active_hooks.tmp_array === null)
    storeActiveHooks();
  return [active_hooks.tmp_array, active_hooks.tmp_fields];
}


function storeActiveHooks() {
  active_hooks.tmp_array = active_hooks.array.slice();
  // Don't want to make the assumption that kInit to kDestroy are indexes 0 to
  // 4. So do this the long way.
  active_hooks.tmp_fields = [];
  active_hooks.tmp_fields[kInit] = async_hook_fields[kInit];
  active_hooks.tmp_fields[kBefore] = async_hook_fields[kBefore];
  active_hooks.tmp_fields[kAfter] = async_hook_fields[kAfter];
  active_hooks.tmp_fields[kDestroy] = async_hook_fields[kDestroy];
  active_hooks.tmp_fields[kPromiseResolve] = async_hook_fields[kPromiseResolve];
}


// Then restore the correct hooks array in case any hooks were added/removed
// during hook callback execution.
function restoreActiveHooks() {
  active_hooks.array = active_hooks.tmp_array;
  async_hook_fields[kInit] = active_hooks.tmp_fields[kInit];
  async_hook_fields[kBefore] = active_hooks.tmp_fields[kBefore];
  async_hook_fields[kAfter] = active_hooks.tmp_fields[kAfter];
  async_hook_fields[kDestroy] = active_hooks.tmp_fields[kDestroy];
  async_hook_fields[kPromiseResolve] = active_hooks.tmp_fields[kPromiseResolve];

  active_hooks.tmp_array = null;
  active_hooks.tmp_fields = null;
}


function createHook(fns) {
  return new AsyncHook(fns);
}


function executionAsyncId() {
  return async_id_fields[kExecutionAsyncId];
}


function triggerAsyncId() {
  return async_id_fields[kTriggerAsyncId];
}

function validateAsyncId(asyncId, type) {
  // Skip validation when async_hooks is disabled
  if (async_hook_fields[kCheck] <= 0) return;

  if (!Number.isSafeInteger(asyncId) || asyncId < -1) {
    fatalError(new errors.RangeError('ERR_INVALID_ASYNC_ID', type, asyncId));
  }
}


// Embedder API //

class AsyncResource {
  constructor(type, triggerAsyncId = initTriggerId()) {
    if (typeof type !== 'string')
      throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'type', 'string');

    // Unlike emitInitScript, AsyncResource doesn't supports null as the
    // triggerAsyncId.
    if (!Number.isSafeInteger(triggerAsyncId) || triggerAsyncId < -1) {
      throw new errors.RangeError('ERR_INVALID_ASYNC_ID',
                                  'triggerAsyncId',
                                  triggerAsyncId);
    }

    this[async_id_symbol] = ++async_id_fields[kAsyncIdCounter];
    this[trigger_async_id_symbol] = triggerAsyncId;

    emitInitScript(
      this[async_id_symbol], type, this[trigger_async_id_symbol], this
    );
  }

  emitBefore() {
    emitBeforeScript(this[async_id_symbol], this[trigger_async_id_symbol]);
    return this;
  }

  emitAfter() {
    emitAfterScript(this[async_id_symbol]);
    return this;
  }

  emitDestroy() {
    emitDestroyScript(this[async_id_symbol]);
    return this;
  }

  asyncId() {
    return this[async_id_symbol];
  }

  triggerAsyncId() {
    return this[trigger_async_id_symbol];
  }
}


function runInAsyncIdScope(asyncId, cb) {
  // Store the async id now to make sure the stack is still good when the ids
  // are popped off the stack.
  const prevId = executionAsyncId();
  pushAsyncIds(asyncId, prevId);
  try {
    cb();
  } finally {
    popAsyncIds(asyncId);
  }
}


// Sensitive Embedder API //

// Increment the internal id counter and return the value. Important that the
// counter increment first. Since it's done the same way in
// Environment::new_async_uid()
function newUid() {
  return ++async_id_fields[kAsyncIdCounter];
}


// Return the triggerAsyncId meant for the constructor calling it. It's up to
// the user to safeguard this call and make sure it's zero'd out when the
// constructor is complete.
function initTriggerId() {
  var triggerAsyncId = async_id_fields[kInitTriggerAsyncId];
  // Reset value after it's been called so the next constructor doesn't
  // inherit it by accident.
  async_id_fields[kInitTriggerAsyncId] = 0;
  if (triggerAsyncId <= 0)
    triggerAsyncId = async_id_fields[kExecutionAsyncId];
  return triggerAsyncId;
}


function setInitTriggerId(triggerAsyncId) {
  // CHECK(Number.isSafeInteger(triggerAsyncId))
  // CHECK(triggerAsyncId > 0)
  async_id_fields[kInitTriggerAsyncId] = triggerAsyncId;
}


function emitInitScript(asyncId, type, triggerAsyncId, resource) {
  validateAsyncId(asyncId, 'asyncId');
  if (triggerAsyncId !== null)
    validateAsyncId(triggerAsyncId, 'triggerAsyncId');
  if (async_hook_fields[kCheck] > 0 &&
      (typeof type !== 'string' || type.length <= 0)) {
    throw new errors.TypeError('ERR_ASYNC_TYPE', type);
  }

  // Short circuit all checks for the common case. Which is that no hooks have
  // been set. Do this to remove performance impact for embedders (and core).
  if (async_hook_fields[kInit] === 0)
    return;

  // This can run after the early return check b/c running this function
  // manually means that the embedder must have used initTriggerId().
  if (triggerAsyncId === null) {
    triggerAsyncId = initTriggerId();
  } else {
    // If a triggerAsyncId was passed, any kInitTriggerAsyncId still must be
    // null'd.
    async_id_fields[kInitTriggerAsyncId] = 0;
  }

  emitInitNative(asyncId, type, triggerAsyncId, resource);
}

function emitHookFactory(symbol, name) {
  // Called from native. The asyncId stack handling is taken care of there
  // before this is called.
  // eslint-disable-next-line func-style
  const fn = function(asyncId) {
    active_hooks.call_depth += 1;
    // Use a single try/catch for all hook to avoid setting up one per
    // iteration.
    try {
      for (var i = 0; i < active_hooks.array.length; i++) {
        if (typeof active_hooks.array[i][symbol] === 'function') {
          active_hooks.array[i][symbol](asyncId);
        }
      }
    } catch (e) {
      fatalError(e);
    } finally {
      active_hooks.call_depth -= 1;
    }

    // Hooks can only be restored if there have been no recursive hook calls.
    // Also the active hooks do not need to be restored if enable()/disable()
    // weren't called during hook execution, in which case
    // active_hooks.tmp_array will be null.
    if (active_hooks.call_depth === 0 && active_hooks.tmp_array !== null) {
      restoreActiveHooks();
    }
  };

  // Set the name property of the anonymous function as it looks good in the
  // stack trace.
  Object.defineProperty(fn, 'name', {
    value: name
  });
  return fn;
}


function emitBeforeScript(asyncId, triggerAsyncId) {
  // Validate the ids. An id of -1 means it was never set and is visible on the
  // call graph. An id < -1 should never happen in any circumstance. Throw
  // on user calls because async state should still be recoverable.
  validateAsyncId(asyncId, 'asyncId');
  validateAsyncId(triggerAsyncId, 'triggerAsyncId');

  pushAsyncIds(asyncId, triggerAsyncId);

  if (async_hook_fields[kBefore] > 0)
    emitBeforeNative(asyncId);
}


function emitAfterScript(asyncId) {
  validateAsyncId(asyncId, 'asyncId');

  if (async_hook_fields[kAfter] > 0)
    emitAfterNative(asyncId);

  popAsyncIds(asyncId);
}


function emitDestroyScript(asyncId) {
  validateAsyncId(asyncId, 'asyncId');

  // Return early if there are no destroy callbacks, or invalid asyncId.
  if (async_hook_fields[kDestroy] === 0 || asyncId <= 0)
    return;
  async_wrap.queueDestroyAsyncId(asyncId);
}


// Used by C++ to call all init() callbacks. Because some state can be setup
// from C++ there's no need to perform all the same operations as in
// emitInitScript.
function emitInitNative(asyncId, type, triggerAsyncId, resource) {
  active_hooks.call_depth += 1;
  // Use a single try/catch for all hook to avoid setting up one per iteration.
  try {
    for (var i = 0; i < active_hooks.array.length; i++) {
      if (typeof active_hooks.array[i][init_symbol] === 'function') {
        active_hooks.array[i][init_symbol](
          asyncId, type, triggerAsyncId,
          resource
        );
      }
    }
  } catch (e) {
    fatalError(e);
  } finally {
    active_hooks.call_depth -= 1;
  }

  // Hooks can only be restored if there have been no recursive hook calls.
  // Also the active hooks do not need to be restored if enable()/disable()
  // weren't called during hook execution, in which case active_hooks.tmp_array
  // will be null.
  if (active_hooks.call_depth === 0 && active_hooks.tmp_array !== null) {
    restoreActiveHooks();
  }
}


// Placing all exports down here because the exported classes won't export
// otherwise.
module.exports = {
  // Public API
  createHook,
  executionAsyncId,
  triggerAsyncId,
  // Embedder API
  AsyncResource,
  runInAsyncIdScope,
  // Sensitive Embedder API
  newUid,
  initTriggerId,
  setInitTriggerId,
  emitInit: emitInitScript,
  emitBefore: emitBeforeScript,
  emitAfter: emitAfterScript,
  emitDestroy: emitDestroyScript,
};
