'use strict';
/**
 * Module dependencies.
 */

const ref = require('ref-napi');
const CIF = require('./cif');
const assert = require('assert');
const debug = require('debug')('ffi:Callback');
const _Callback = require('./bindings').Callback;

// Function used to report errors to the current process event loop,
// When user callback function gets gced.
function errorReportCallback (err) {
  if (err) {
    process.nextTick(function () {
      if (typeof err === 'string') {
        throw new Error(err);
      } else {
        throw err;
      }
    })
  }
}

/**
 * Turns a JavaScript function into a C function pointer.
 * The function pointer may be used in other C functions that
 * accept C callback functions.
 */

function Callback (retType, argTypes, abi, func) {
  debug('creating new Callback');

  if (typeof abi === 'function') {
    func = abi;
    abi = undefined;
  }

  // check args
  assert(!!retType, 'expected a return "type" object as the first argument');
  assert(Array.isArray(argTypes), 'expected Array of arg "type" objects as the second argument');
  assert.equal(typeof func, 'function', 'expected a function as the third argument');

  // normalize the "types" (they could be strings, so turn into real type
  // instances)
  retType = ref.coerceType(retType);
  argTypes = argTypes.map(ref.coerceType);

  // create the `ffi_cif *` instance
  const cif = CIF(retType, argTypes, abi);
  const argc = argTypes.length;

  const callback = _Callback(cif, retType.size, argc, errorReportCallback, (retval, params) => {
    debug('Callback function being invoked')
    try {
      const args = [];
      for (var i = 0; i < argc; i++) {
        const type = argTypes[i];
        const argPtr = params.readPointer(i * ref.sizeof.pointer, type.size);
        argPtr.type = type;
        args.push(argPtr.deref());
      }

      // Invoke the user-given function
      const result = func.apply(null, args);
      try {
        ref.set(retval, 0, result, retType);
      } catch (e) {
        e.message = 'error setting return value - ' + e.message;
        throw e;
      }
    } catch (e) {
      return e;
    }
  });

  return callback;
}

module.exports = Callback;
