'use strict'

const refs = {
  exit: [],
  beforeExit: []
}
const functions = {
  exit: onExit,
  beforeExit: onBeforeExit
}

let registry

function ensureRegistry () {
  if (registry === undefined) {
    registry = new FinalizationRegistry(clear)
  }
}

function install (event) {
  if (refs[event].length > 0) {
    return
  }

  process.on(event, functions[event])
}

function uninstall (event) {
  if (refs[event].length > 0) {
    return
  }
  process.removeListener(event, functions[event])
  if (refs.exit.length === 0 && refs.beforeExit.length === 0) {
    registry = undefined
  }
}

function onExit () {
  callRefs('exit')
}

function onBeforeExit () {
  callRefs('beforeExit')
}

function callRefs (event) {
  for (const ref of refs[event]) {
    const obj = ref.deref()
    const fn = ref.fn

    // This should always happen, however GC is
    // undeterministic so it might not happen.
    /* istanbul ignore else */
    if (obj !== undefined) {
      fn(obj, event)
    }
  }
  refs[event] = []
}

function clear (ref) {
  for (const event of ['exit', 'beforeExit']) {
    const index = refs[event].indexOf(ref)
    refs[event].splice(index, index + 1)
    uninstall(event)
  }
}

function _register (event, obj, fn) {
  if (obj === undefined) {
    throw new Error('the object can\'t be undefined')
  }
  install(event)
  const ref = new WeakRef(obj)
  ref.fn = fn

  ensureRegistry()
  registry.register(obj, ref)
  refs[event].push(ref)
}

function register (obj, fn) {
  _register('exit', obj, fn)
}

function registerBeforeExit (obj, fn) {
  _register('beforeExit', obj, fn)
}

function unregister (obj) {
  if (registry === undefined) {
    return
  }
  registry.unregister(obj)
  for (const event of ['exit', 'beforeExit']) {
    refs[event] = refs[event].filter((ref) => {
      const _obj = ref.deref()
      return _obj && _obj !== obj
    })
    uninstall(event)
  }
}

module.exports = {
  register,
  registerBeforeExit,
  unregister
}
