#include <stdlib.h>

#include "api/getDeviceInfoList.h"
#include "api/FTDI_DeviceInfo.h"
#include "module_data.h"
#include "ftd2xx.h"
#include "utils.h"

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

  // Pointer to global module data
  module_data_t* module_data;

  // Data passed to complete_callback
  FT_STATUS ftStatus;
  FT_DEVICE_LIST_INFO_NODE* device_info_list;
  DWORD nb_devices;

} 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;
  DWORD nb_devices = 0;

  // Initialize async instance data
  async_data->device_info_list = NULL;
  async_data->nb_devices = 0;

  // Create FTDI device info list
  async_data->ftStatus = FT_CreateDeviceInfoList(&nb_devices);
  if(async_data->ftStatus != FT_OK) return;

  // If devices are detected, query device info
  if(nb_devices > 0) {

    // Allocate dynamic memory for device info list based on nb_devices
    async_data->device_info_list = malloc(sizeof(FT_DEVICE_LIST_INFO_NODE) * nb_devices);
    if(!async_data->device_info_list) {
      async_data->ftStatus = FT_INSUFFICIENT_RESOURCES;
      return;
    }
    
    // Fill the allocated memory with the list of device info
    async_data->ftStatus = FT_GetDeviceInfoList(async_data->device_info_list, &nb_devices);
    if(async_data->ftStatus != FT_OK) return;

    // Write the number of devices if all went good
    async_data->nb_devices = nb_devices;
  }
}


// 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) {
  if(status != napi_ok) return;
  async_data_t* async_data = (async_data_t*) data;

  // Get FTDI_DeviceInfo class from its reference
  napi_value device_info_class;
  utils_check(napi_get_reference_value(env, async_data->module_data->device_info_class_ref, &device_info_class));

  // Create a new array containing the FTDI_DeviceInfo instances
  napi_value info_array;
  size_t info_array_length = 0;
  utils_check(napi_create_array(env, &info_array));
  
  // Check that a device info list was allocated (no allocation if no devices were detected)
  if(async_data->nb_devices && async_data->device_info_list) {

    // Browse the new connected devices from the list returned by FTDI `FT_GetDeviceInfoList`
    for(uint32_t i = 0; i < async_data->nb_devices; i++) {
      FT_DEVICE_LIST_INFO_NODE device_info = async_data->device_info_list[i];

      // Convert values to JavaScript
      napi_value serial_number, description, type, is_open, usb_speed, usb_vid, usb_pid, usb_loc_id;
      utils_check(napi_create_string_utf8(env, device_info.SerialNumber, NAPI_AUTO_LENGTH, &serial_number));
      utils_check(napi_create_string_utf8(env, device_info.Description, NAPI_AUTO_LENGTH, &description));
      type = utils_ft_device_to_js_string(env, (FT_DEVICE)(device_info.Type));
      utils_check(napi_get_boolean(env, (device_info.Flags & 0b0001), &is_open));
      utils_check(napi_create_string_utf8(env, (device_info.Flags & 0b0010) ? "high-speed" : "full-speed", NAPI_AUTO_LENGTH, &usb_speed));
      utils_check(napi_create_uint32(env, (device_info.ID >> 16), &usb_vid));
      utils_check(napi_create_uint32(env, (device_info.ID & 0xFFFF), &usb_pid));
      utils_check(napi_create_uint32(env, device_info.LocId, &usb_loc_id));

      // Create a FTDI_DeviceInfo instance representing the device
      napi_value device_info_instance;
      napi_value argv[] = { serial_number, description, type, is_open, usb_vid, usb_pid, usb_loc_id, usb_speed };
      utils_check(napi_new_instance(env, device_info_class, sizeof(argv)/sizeof(argv[0]), argv, &device_info_instance));

      // Add the device info to the array
      utils_check(napi_set_element(env, info_array, (uint32_t)(info_array_length++), device_info_instance));
    }

    // Free device info list
    free(async_data->device_info_list);
  }

  // Resolve the JavaScript `Promise` with the return value
  utils_check(napi_resolve_deferred(env, async_data->deferred, info_array));

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

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


// Create a deferred JavaScript `Promise` and an async queue work item
napi_value getDeviceInfoList(napi_env env, napi_callback_info info) {

  // 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;
  
  // Copy the global module data pointer to the async instance data
  utils_check(napi_get_cb_info(env, info, NULL, NULL, NULL, (void**)(&(async_data->module_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, "getDeviceInfoList", 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;
}
