#include <stdlib.h>
#include <string.h>

#include "api/FTDI_Device.h"
#include "ftd2xx.h"
#include "utils.h"

typedef struct {
  // Node-API variables
  napi_async_work async_work;
  napi_deferred deferred;

  // Data passed to execute_callback
  device_instance_data_t* instance_data;
  uint32_t rx_bytes_to_read;
  uint8_t* rx_buffer;

  // Data passed to complete_callback
  FT_STATUS ftStatus;
  DWORD rx_bytes_returned;

} async_data_t;


// This function runs on a worker thread.
// It has no access to the JavaScript. Only FTDI functions are called here.
static void execute_callback(napi_env env, void* data) {
  (void) env; // hide unused parameter warning
  async_data_t* async_data = (async_data_t*) data;

  // Read from FTDI device (this is a blocking function)
  async_data->ftStatus = FT_Read(async_data->instance_data->ftHandle, async_data->rx_buffer,
                                  async_data->rx_bytes_to_read, &(async_data->rx_bytes_returned));
}


// This function runs on the main thread after `execute_callback` exits.
// JavaScript functions are called here to convert data generated by FTDI.
static void complete_callback(napi_env env, napi_status status, void* data) {
  (void) status; // hide unused parameter warning
  void* array_buffer_data = NULL;
  napi_value array_buffer, uint8_array = NULL;
  async_data_t* async_data = (async_data_t*) data;

  // Manage FTDI error if any. Otherwise, process the return value
  if(!utils_check(async_data->ftStatus == FT_IO_ERROR, "USB was lost during read operation", ERR_USBLOST)
    && !utils_check(FT_|async_data->ftStatus)) { // manage other errors

    // Create JavaScript Uint8Array containing the read data
    utils_check(napi_create_arraybuffer(env, async_data->rx_bytes_returned, &array_buffer_data, &array_buffer));
    memcpy(array_buffer_data, async_data->rx_buffer, async_data->rx_bytes_returned);
    utils_check(napi_create_typedarray(env, napi_uint8_array, async_data->rx_bytes_returned, array_buffer, 0, &uint8_array));
  }

  // Resolve the JavaScript `Promise`:
  bool is_exception_pending;
  napi_is_exception_pending(env, &is_exception_pending);
  if(is_exception_pending) {
    // If an exception is pending, clear it to prevent Node.js from crashing
    napi_value error;
    napi_get_and_clear_last_exception(env, &error);

    // Instead reject the JavaScript `Promise` with the error
    napi_reject_deferred(env, async_data->deferred, error);

  } else {
    // Else resolve the JavaScript `Promise` with the return value
    napi_resolve_deferred(env, async_data->deferred, uint8_array);
  }

  // Clean up the work item associated with this run
  napi_delete_async_work(env, async_data->async_work);

  // Free async instance data structure
  free(async_data->rx_buffer);
  free(async_data);
}


// Create a deferred JavaScript `Promise` and an async queue work item
napi_value device_read(napi_env env, napi_callback_info info) {
  // Get JavaScript `this` corresponding to this instance of the class and `argc`/`argv` passed to the function
  #define NB_ARGS 1 // number of expected arguments
  size_t argc = NB_ARGS; // size of the argv buffer
  napi_value this_arg, argv[NB_ARGS];
  utils_check(napi_get_cb_info(env, info, &argc, argv, &this_arg, NULL));
  if(utils_check(argc < NB_ARGS, "Missing argument", ERR_MISSARG)) return NULL;

  // Check that the number of bytes argument is a Number
  napi_valuetype type;
  utils_check(napi_typeof(env, argv[0], &type));
  if(utils_check(type != napi_number, "The number of bytes to read must be a number", ERR_WRONGARG)) return NULL;

  // Allocate memory for async instance data structure
  async_data_t* async_data = malloc(sizeof(async_data_t));
  if(utils_check(async_data == NULL, "Malloc failed", ERR_MALLOC)) return NULL;

  // Allocate memory for RX buffer
  utils_check(napi_get_value_uint32(env, argv[0], &(async_data->rx_bytes_to_read)));
  async_data->rx_buffer = malloc(async_data->rx_bytes_to_read);
  if(utils_check(async_data->rx_buffer == NULL, "Malloc failed", ERR_MALLOC)) return NULL;

  // Get the class instance data containing FTDI device handle
  utils_check(napi_unwrap(env, this_arg, (void**)&(async_data->instance_data)));

  // Create a deferred `Promise` which we will resolve at the completion of the work
  napi_value promise;
  utils_check(napi_create_promise(env, &(async_data->deferred), &promise));

  // Create an async work item, passing in the addon data, which will give the worker thread access to the `Promise`
  napi_value name;
  utils_check(napi_create_string_utf8(env, "deviceRead", NAPI_AUTO_LENGTH, &name));
  utils_check(napi_create_async_work(env, NULL, name, execute_callback, complete_callback, async_data, &(async_data->async_work)));

  // Queue the work item for execution
  utils_check(napi_queue_async_work(env, async_data->async_work));

  // This causes created `Promise` to be returned to JavaScript
  return promise;
}
