/* Copyright 2018 Streampunk Media Ltd.

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

/* -LICENSE-START-
 ** Copyright (c) 2010 Blackmagic Design
 **
 ** Permission is hereby granted, free of charge, to any person or organization
 ** obtaining a copy of the software and accompanying documentation covered by
 ** this license (the "Software") to use, reproduce, display, distribute,
 ** execute, and transmit the Software, and to prepare derivative works of the
 ** Software, and to permit third-parties to whom the Software is furnished to
 ** do so, all subject to the following:
 **
 ** The copyright notices in the Software and this entire statement, including
 ** the above license grant, this restriction and the following disclaimer,
 ** must be included in all copies of the Software, in whole or in part, and
 ** all derivative works of the Software, unless such copies or derivative
 ** works are solely in the form of machine-executable object code generated by
 ** a source language processor.
 **
 ** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 ** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 ** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 ** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 ** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 ** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 ** DEALINGS IN THE SOFTWARE.
 ** -LICENSE-END-
 */

#include "capture_promise.h"

HRESULT captureThreadsafe::VideoInputFrameArrived(
  IDeckLinkVideoInputFrame *videoFrame,
  IDeckLinkAudioInputPacket *audioPacket) {

  napi_status status, hangover;
  status = napi_acquire_threadsafe_function(tsFn);
  if (status != napi_ok) {
    printf("DEBUG: Failed to acquire NAPI threadsafe function on capture.");
    return E_FAIL;
  }

  videoFrame->AddRef();
  if (audioPacket != nullptr) {
    audioPacket->AddRef();
  }
  frameData* data = (frameData*) malloc(sizeof(frameData));
  data->videoFrame = videoFrame;
  data->audioPacket = audioPacket;
  hangover = napi_call_threadsafe_function(tsFn, data, napi_tsfn_nonblocking);
  if (hangover != napi_ok) {
    printf("DEBUG: Failed to call NAPI threadsafe function on capture.");
  }

  status = napi_release_threadsafe_function(tsFn, napi_tsfn_release);
  if (status != napi_ok) {
    printf("DEBUG: Failed to acquire NAPI threadsafe function on capture.");
    return E_FAIL;
  }

  return (hangover == napi_ok) ? S_OK : E_FAIL;
};

HRESULT captureThreadsafe::VideoInputFormatChanged(
  BMDVideoInputFormatChangedEvents notificationEvents,
  IDeckLinkDisplayMode *newDisplayMode,
  BMDDetectedVideoInputFormatFlags detectedSignalFlags) {

  return E_FAIL;
}

void finalizeCaptureCarrier(napi_env env, void* finalize_data, void* finalize_hint) {
  // printf("Finalizing capture threadsafe.\n");
  captureThreadsafe* c = (captureThreadsafe*) finalize_data;
  delete c;
}

void finalizeVideoBuffer(napi_env env, void* finalize_data, void* finalize_hint) {
  napi_status status;
  int64_t externalMemory;
  IDeckLinkVideoInputFrame* video = (IDeckLinkVideoInputFrame*) finalize_hint;
  status = napi_adjust_external_memory(env, -video->GetRowBytes()*video->GetHeight(),
    &externalMemory);
  FLOATING_STATUS;
  video->Release();
  // printf("Releasing video frame - ext mem now %li\n", externalMemory);
}

void finalizeAudioPacket(napi_env env, void* finalize_data, void* finalize_hint) {
  napi_status status;
  int64_t externalMemory = 0;
  audioData* audio = (audioData*) finalize_hint;
  status = napi_adjust_external_memory(env, -((int64_t) audio->dataSize), &externalMemory);
  FLOATING_STATUS;
  audio->audioPacket->Release();
  // printf("Releasing audio packet - ext mem now %li\n", externalMemory);
  free(audio);
}

napi_value stopStreams(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value value, param, capture;
  captureThreadsafe* crts;
  HRESULT hresult;

  size_t argc = 0;
  status = napi_get_cb_info(env, info, &argc, nullptr, &capture, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, capture, "deckLinkInput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &crts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already stopped.");
  CHECK_STATUS;

  hresult = crts->deckLinkInput->StopStreams();
  if (hresult != S_OK) NAPI_THROW_ERROR("Unable to stop streams. May be already stopped?");
  hresult = crts->deckLinkInput->DisableVideoInput();
  if (hresult != S_OK) NAPI_THROW_ERROR("Unable to disable video input.");
	hresult = crts->deckLinkInput->SetCallback(NULL);
  if (hresult != S_OK) NAPI_THROW_ERROR("Unable to unset callback for decklink input.");

  status = napi_release_threadsafe_function(crts->tsFn, napi_tsfn_release);
  CHECK_STATUS;

  status = napi_get_undefined(env, &value);
  CHECK_STATUS;

  status = napi_set_named_property(env, capture, "deckLinkInput", value);
  CHECK_STATUS;

  crts->deckLinkInput->Release();
  crts->deckLinkInput = nullptr;

  return value;
}

napi_value pauseStreams(napi_env env, napi_callback_info info) {
  napi_status status;
  napi_value value, param, capture;
  captureThreadsafe* crts;
  HRESULT hresult;

  size_t argc = 0;
  status = napi_get_cb_info(env, info, &argc, nullptr, &capture, nullptr);
  CHECK_STATUS;

  status = napi_get_named_property(env, capture, "deckLinkInput", &param);
  CHECK_STATUS;
  status = napi_get_value_external(env, param, (void**) &crts);
  if (status == napi_invalid_arg) NAPI_THROW_ERROR("Already stopped.");
  CHECK_STATUS;

  hresult = crts->deckLinkInput->PauseStreams();
  if (hresult != S_OK) NAPI_THROW_ERROR("Unable to pause or restart streams.");

  status = napi_get_undefined(env, &value);
  CHECK_STATUS;
  return value;
}

void captureExecute(napi_env env, void* data) {
  captureCarrier* c = (captureCarrier*) data;

  IDeckLinkIterator* deckLinkIterator;
  IDeckLink* deckLink;
  IDeckLinkInput* deckLinkInput;
  HRESULT hresult;

  #ifdef WIN32
  hresult = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
  CoCreateInstance(CLSID_CDeckLinkIterator, NULL, CLSCTX_ALL, IID_IDeckLinkIterator, (void**)&deckLinkIterator);
  #else
  deckLinkIterator = CreateDeckLinkIteratorInstance();
  #endif
  if (deckLinkIterator == nullptr) {
    c->status = MACADAM_ERROR_START;
    c->errorMsg = "Unable to load DeckLinkAPI.";
    return;
  }

  for ( uint32_t x = 0 ; x <= c->deviceIndex ; x++ ) {
    if (deckLinkIterator->Next(&deckLink) != S_OK) {
      deckLinkIterator->Release();
      c->status = MACADAM_OUT_OF_BOUNDS;
      c->errorMsg = "Device index exceeds the number of installed devices.";
      return;
    }
  }

  deckLinkIterator->Release();

  if (deckLink->QueryInterface(IID_IDeckLinkInput, (void **)&deckLinkInput) != S_OK) {
    deckLink->Release();
    c->status = MACADAM_NO_INPUT;
    c->errorMsg = "Could not obtain the DeckLink Input interface. Does the device have an input?";
    return;
  }

  deckLink->Release();
  c->deckLinkInput = deckLinkInput;

  BMDDisplayModeSupport supported;

  hresult = deckLinkInput->DoesSupportVideoMode(c->requestedDisplayMode,
    c->requestedPixelFormat, bmdVideoInputFlagDefault,
    &supported, &c->selectedDisplayMode);
  if (hresult != S_OK) {
    c->status = MACADAM_CALL_FAILURE;
    c->errorMsg = "Unable to determine if video mode is supported by input device.";
    return;
  }
  switch (supported) {
    case bmdDisplayModeSupported:
      break;
    case bmdDisplayModeSupportedWithConversion:
      c->status = MACADAM_NO_CONVERESION;
      c->errorMsg = "Display mode is supported via conversion and not by macadam.";
      return;
    default:
      c->status = MACADAM_MODE_NOT_SUPPORTED;
      c->errorMsg = "Requested display mode is not supported.";
      return;
  }

  hresult = deckLinkInput->EnableVideoInput(c->requestedDisplayMode,
    c->requestedPixelFormat, bmdVideoInputFlagDefault);
  switch (hresult) {
    case E_INVALIDARG: // Should have been picked up by DoesSupportVideoMode
      c->status = MACADAM_INVALID_ARGS;
      c->errorMsg = "Invalid arguments used to enable video input.";
      return;
    case E_ACCESSDENIED:
      c->status = MACADAM_ACCESS_DENIED;
      c->errorMsg = "Unable to access the hardware or input stream is currently active.";
      return;
    case E_OUTOFMEMORY:
      c->status = MACADAM_OUT_OF_MEMORY;
      c->errorMsg = "Unable to create a new video frame - out of memory.";
      return;
    case E_FAIL:
      c->status = MACADAM_CALL_FAILURE;
      c->errorMsg = "Failed to enable video input.";
      return;
    case S_OK:
      break;
  }

  if (c->channels > 0) {
    hresult = deckLinkInput->EnableAudioInput(c->requestedSampleRate,
      c->requestedSampleType, c->channels);
    switch (hresult)  {
      case E_INVALIDARG:
        c->status = MACADAM_INVALID_ARGS;
        c->errorMsg = "Invalid arguments used to enable audio input. BMD supports 48kHz, 16- or 32-bit integer only.";
        return;
      case E_FAIL:
        c->status = MACADAM_CALL_FAILURE;
        c->errorMsg = "Failed to enable audio input.";
        return;
      case S_OK:
        break;
    }
  }
}

void captureComplete(napi_env env, napi_status asyncStatus, void* data) {
  captureCarrier* c = (captureCarrier*) data;
  napi_value param, paramPart, result, asyncName;
  BMDTimeValue frameRateDuration;
  BMDTimeScale frameRateScale;
  HRESULT hresult;

  if (asyncStatus != napi_ok) {
    c->status = asyncStatus;
    c->errorMsg = "Async capture creator failed to complete.";
  }
  REJECT_STATUS;

  c->status = napi_create_object(env, &result);
  REJECT_STATUS;
  c->status = napi_create_string_utf8(env, "capture", NAPI_AUTO_LENGTH, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "type", param);
  REJECT_STATUS;

  #ifdef WIN32
  BSTR displayModeBSTR = NULL;
  hresult = c->selectedDisplayMode->GetName(&displayModeBSTR);
  if (hresult == S_OK) {
    _bstr_t deviceName(displayModeBSTR, false);
    c->status = napi_create_string_utf8(env, (char*) deviceName, NAPI_AUTO_LENGTH, &param);
    REJECT_STATUS;
  }
  #elif __APPLE__
  CFStringRef displayModeCFString = NULL;
  hresult = c->selectedDisplayMode->GetName(&displayModeCFString);
  if (hresult == S_OK) {
    char displayModeName[64];
    CFStringGetCString(displayModeCFString, displayModeName, sizeof(displayModeName), kCFStringEncodingMacRoman);
    CFRelease(displayModeCFString);
    c->status = napi_create_string_utf8(env, displayModeName, NAPI_AUTO_LENGTH, &param);
    REJECT_STATUS;
  }
  #else
  char* displayModeName;
  hresult = c->selectedDisplayMode->GetName((const char **) &displayModeName);
  if (hresult == S_OK) {
    c->status = napi_create_string_utf8(env, displayModeName, NAPI_AUTO_LENGTH, &param);
    free(displayModeName);
    REJECT_STATUS;
  }
  #endif

  c->status = napi_set_named_property(env, result, "displayModeName", param);
  REJECT_STATUS;

  c->status = napi_create_int32(env, c->selectedDisplayMode->GetWidth(), &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "width", param);
  REJECT_STATUS;

  c->status = napi_create_int32(env, c->selectedDisplayMode->GetHeight(), &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "height", param);
  REJECT_STATUS;

  switch (c->selectedDisplayMode->GetFieldDominance()) {
    case bmdLowerFieldFirst:
      c->status = napi_create_string_utf8(env, "lowerFieldFirst", NAPI_AUTO_LENGTH, &param);
      break;
    case bmdUpperFieldFirst:
      c->status = napi_create_string_utf8(env, "upperFieldFirst", NAPI_AUTO_LENGTH, &param);
      break;
    case bmdProgressiveFrame:
      c->status = napi_create_string_utf8(env, "progressiveFrame", NAPI_AUTO_LENGTH, &param);
      break;
    default:
      c->status = napi_create_string_utf8(env, "unknown", NAPI_AUTO_LENGTH, &param);
      break;
  }
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "fieldDominance", param);
  REJECT_STATUS;

  hresult = c->selectedDisplayMode->GetFrameRate(&frameRateDuration, &frameRateScale);
  if (hresult == S_OK) {
    c->status = napi_create_array(env, &param);
    REJECT_STATUS;
    c->status = napi_create_int64(env, frameRateDuration, &paramPart);
    REJECT_STATUS;
    c->status = napi_set_element(env, param, 0, paramPart);
    REJECT_STATUS;
    c->status = napi_create_int64(env, frameRateScale, &paramPart);
    REJECT_STATUS;
    c->status = napi_set_element(env, param, 1, paramPart);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "frameRate", param);
    REJECT_STATUS;
  }

  uint32_t pixelFormatIndex = 0;

  while ((gKnownPixelFormats[pixelFormatIndex] != 0) &&
      (gKnownPixelFormatNames[pixelFormatIndex] != NULL)) {
    if (c->requestedPixelFormat == gKnownPixelFormats[pixelFormatIndex]) {
      c->status = napi_create_string_utf8(env, gKnownPixelFormatNames[pixelFormatIndex],
        NAPI_AUTO_LENGTH, &param);
      REJECT_STATUS;
      c->status = napi_set_named_property(env, result, "pixelFormat", param);
      REJECT_STATUS;
      break;
    }
    pixelFormatIndex++;
  }

  c->status = napi_get_boolean(env, (c->channels > 0), &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "audioEnabled", param);
  REJECT_STATUS;

  if (c->channels > 0) {
    c->status = napi_create_int32(env, c->requestedSampleRate, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "sampleRate", param);
    REJECT_STATUS;

    c->status = napi_create_int32(env, c->requestedSampleType, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "sampleType", param);
    REJECT_STATUS;

    c->status = napi_create_int32(env, c->channels, &param);
    REJECT_STATUS;
    c->status = napi_set_named_property(env, result, "channels", param);
    REJECT_STATUS;
  }

  c->status = napi_create_function(env, "pause", NAPI_AUTO_LENGTH, pauseStreams,
    nullptr, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "pause", param);
  REJECT_STATUS;

  c->status = napi_create_function(env, "stop", NAPI_AUTO_LENGTH, stopStreams,
    nullptr, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "stop", param);
  REJECT_STATUS;

  c->status = napi_create_function(env, "frame", NAPI_AUTO_LENGTH, framePromise,
    nullptr, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "frame", param);
  REJECT_STATUS;

  captureThreadsafe* crts = new captureThreadsafe;
  crts->deckLinkInput = c->deckLinkInput;
  c->deckLinkInput = nullptr;
  crts->displayMode = c->selectedDisplayMode;
  c->selectedDisplayMode = nullptr;
  crts->timeScale = frameRateScale;
  crts->pixelFormat = c->requestedPixelFormat;
  crts->roughFps = (uint16_t) (frameRateScale / frameRateDuration); // Used for timecode formatting
  crts->channels = c->channels;
  if (c->channels > 0) {
    crts->sampleRate = c->requestedSampleRate;
    crts->sampleType = c->requestedSampleType;
    crts->sampleByteFactor = c->channels * (crts->sampleType / 8);
  }

  hresult = crts->deckLinkInput->SetCallback(crts);
  if (hresult != S_OK) {
    c->status = MACADAM_CALL_FAILURE;
    c->errorMsg = "Unable to set callback for deck link input.";
    REJECT_STATUS;
  }

  c->status = napi_create_string_utf8(env, "capture", NAPI_AUTO_LENGTH, &asyncName);
  REJECT_STATUS;
  c->status = napi_create_function(env, "nop", NAPI_AUTO_LENGTH, nop, nullptr, &param);
  REJECT_STATUS;
  c->status = napi_create_threadsafe_function(env, param, nullptr, asyncName,
    20, 1, nullptr, captureTsFnFinalize, crts, frameResolver, &crts->tsFn);
  REJECT_STATUS;

  c->status = napi_create_external(env, crts, finalizeCaptureCarrier, nullptr, &param);
  REJECT_STATUS;
  c->status = napi_set_named_property(env, result, "deckLinkInput", param);
  REJECT_STATUS;

  napi_status status;
  status = napi_resolve_deferred(env, c->_deferred, result);
  FLOATING_STATUS;

  tidyCarrier(env, c);
}

napi_value capture(napi_env env, napi_callback_info info) {
  napi_value options, param, promise, resourceName;
  napi_valuetype type;
  bool isArray;
  captureCarrier* c = new captureCarrier;

  c->status = napi_create_promise(env, &c->_deferred, &promise);
  REJECT_RETURN;

  c->requestedDisplayMode = bmdModeHD1080i50;
  c->requestedPixelFormat = bmdFormat10BitYUV;
  size_t argc = 1;
  napi_value args[1];
  c->status = napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
  REJECT_RETURN;

  if (argc >= 1) {
    c->status = napi_typeof(env, args[0], &type);
    REJECT_RETURN;
    c->status = napi_is_array(env, args[0], &isArray);
    REJECT_RETURN;
    if ((type != napi_object) || (isArray == true)) REJECT_ERROR_RETURN(
        "Options provided to capture create must be an object and not an array.",
        MACADAM_INVALID_ARGS);
    options = args[0];
  }
  else {
    c->status = napi_create_object(env, &options);
    REJECT_RETURN;
  }

  c->status = napi_get_named_property(env, options, "deviceIndex", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_number) REJECT_ERROR_RETURN(
      "Device index must be a number.", MACADAM_INVALID_ARGS);
    c->status = napi_get_value_uint32(env, param, &c->deviceIndex);
    REJECT_RETURN;
  }

  c->status = napi_get_named_property(env, options, "displayMode", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_number) REJECT_ERROR_RETURN(
      "Display mode must be an enumeration value.", MACADAM_INVALID_ARGS);
    c->status = napi_get_value_uint32(env, param, (uint32_t *) &c->requestedDisplayMode);
    REJECT_RETURN;
  }

  c->status = napi_get_named_property(env, options, "pixelFormat", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_number) REJECT_ERROR_RETURN(
      "Pixel format must be an enumeration value.", MACADAM_INVALID_ARGS);
    c->status = napi_get_value_uint32(env, param, (uint32_t *) &c->requestedPixelFormat);
    REJECT_RETURN;
  }

  c->status = napi_get_named_property(env, options, "channels", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_number) REJECT_ERROR_RETURN(
      "Audio channel count must be a number.", MACADAM_INVALID_ARGS);
    c->status = napi_get_value_uint32(env, param, &c->channels);
    REJECT_RETURN;
  }

  c->status = napi_get_named_property(env, options, "sampleRate", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_number) REJECT_ERROR_RETURN(
      "Audio sample rate must be an enumeration value.", MACADAM_INVALID_ARGS);
    c->status = napi_get_value_uint32(env, param, (uint32_t *) &c->requestedSampleRate);
    REJECT_RETURN;
  }

  c->status = napi_get_named_property(env, options, "sampleType", &param);
  REJECT_RETURN;
  c->status = napi_typeof(env, param, &type);
  REJECT_RETURN;
  if (type != napi_undefined) {
    if (type != napi_number) REJECT_ERROR_RETURN(
      "Audio sample type must be an enumeration value.", MACADAM_INVALID_ARGS);
    c->status = napi_get_value_uint32(env, param, (uint32_t *) &c->requestedSampleType);
    REJECT_RETURN;
  }

  c->status = napi_create_string_utf8(env, "CreateCapture", NAPI_AUTO_LENGTH, &resourceName);
  REJECT_RETURN;
  c->status = napi_create_async_work(env, NULL, resourceName, captureExecute,
    captureComplete, c, &c->_request);
  REJECT_RETURN;
  c->status = napi_queue_async_work(env, c->_request);
  REJECT_RETURN;

  return promise;
}

napi_value framePromise(napi_env env, napi_callback_info info) {
  napi_value promise, capture, param;
  captureThreadsafe* crts;
  frameCarrier* c = new frameCarrier;
  HRESULT hresult;

  c->status = napi_create_promise(env, &c->_deferred, &promise);
  REJECT_RETURN;

  size_t argc = 0;
  c->status = napi_get_cb_info(env, info, &argc, nullptr, &capture, nullptr);
  REJECT_RETURN;

  c->status = napi_get_named_property(env, capture, "deckLinkInput", &param);
  REJECT_RETURN;
  c->status = napi_get_value_external(env, param, (void**) &crts);
  if (c->status == napi_invalid_arg) REJECT_ERROR_RETURN(
    "Cannot request frames after stream stop.", MACADAM_ALREADY_STOPPED);
  REJECT_RETURN;

  if (!crts->started) {
    hresult = crts->deckLinkInput->StartStreams();
    switch (hresult) {
      case E_FAIL:
        REJECT_ERROR_RETURN("Call to start streams failed.",
          MACADAM_CALL_FAILURE);
        break;
      case E_UNEXPECTED:
        REJECT_ERROR_RETURN("Video and/or audio inputs are not enabled.",
          MACADAM_CALL_FAILURE);
        break;
      case E_ACCESSDENIED: // Streams are already running
      case S_OK:
        break;
    }
    crts->started = true;
  }

  crts->framePromises.push(c);

  return promise;
}

void frameResolver(napi_env env, napi_value jsCb, void* context, void* data) {
  napi_value result, obj, param;
  captureThreadsafe* crts = (captureThreadsafe*) context;
  frameData* frame = (frameData*) data;
  frameCarrier* c = nullptr;
  BMDTimeValue frameTime;
  BMDTimeValue frameDuration;
  BMDTimeValue packetTime;
  BMDFrameFlags videoFlags;
  BMDTimecodeUserBits userBits;
  int32_t rowBytes, height, sampleFrameCount;
  int64_t externalMemory;
  void* bytes;
  IDeckLinkTimecode* timecode;
  audioData* audioFinalizeData;
  HRESULT hresult;
  // TODO : Add support for ancillary data

  //printf("Received an input frame %lix%li\n", frame->videoFrame->GetWidth(),
  //  frame->videoFrame->GetHeight());

  if (!crts->framePromises.empty()) {
    c = crts->framePromises.front();

    c->status = napi_create_object(env, &result);
    REJECT_BAIL;
    c->status = napi_create_string_utf8(env, "frame", NAPI_AUTO_LENGTH, &param);
    REJECT_BAIL;
    c->status = napi_set_named_property(env, result, "type", param);
    REJECT_BAIL;
    c->status = napi_create_object(env, &obj);
    REJECT_BAIL;
    c->status = napi_set_named_property(env, result, "video", obj);
    REJECT_BAIL;

    c->status = napi_create_string_utf8(env, "videoFrame", NAPI_AUTO_LENGTH, &param);
    REJECT_BAIL;
    c->status = napi_set_named_property(env, obj, "type", param);
    REJECT_BAIL;

    c->status = napi_create_int32(env, frame->videoFrame->GetWidth(), &param);
    REJECT_BAIL;
    c->status = napi_set_named_property(env, obj, "width", param);
    REJECT_BAIL;

    height = frame->videoFrame->GetHeight();
    c->status = napi_create_int32(env, height, &param);
    REJECT_BAIL;
    c->status = napi_set_named_property(env, obj, "height", param);
    REJECT_BAIL;

    rowBytes = frame->videoFrame->GetRowBytes();
    c->status = napi_create_int32(env, rowBytes, &param);
    REJECT_BAIL;
    c->status = napi_set_named_property(env, obj, "rowBytes", param);
    REJECT_BAIL;

    hresult = frame->videoFrame->GetStreamTime(&frameTime, &frameDuration, crts->timeScale);
    if (hresult != S_OK) {
      c->errorMsg = "Failed to retrieve frame time for video frame.";
      c->status = MACADAM_CALL_FAILURE;
      REJECT_BAIL;
    }
    c->status = napi_create_int32(env, frameTime, &param);
    REJECT_BAIL;
    c->status = napi_set_named_property(env, obj, "frameTime", param);
    REJECT_BAIL;
    c->status = napi_create_int32(env, frameDuration, &param);
    REJECT_BAIL;
    c->status = napi_set_named_property(env, obj, "frameDuration", param);
    REJECT_BAIL;

    hresult = frame->videoFrame->GetBytes(&bytes);
    if (hresult != S_OK) {
      c->errorMsg = "Failed to access the byte buffer of a video frame.";
      c->status = MACADAM_CALL_FAILURE;
      REJECT_BAIL;
    }
    c->status = napi_create_external_buffer(env, rowBytes*height, bytes,
      finalizeVideoBuffer, frame->videoFrame, &param);
    REJECT_BAIL;
    c->status = napi_set_named_property(env, obj, "data", param);
    REJECT_BAIL;
    c->status = napi_adjust_external_memory(env, rowBytes*height, &externalMemory);
    // printf("External memory %li\n", externalMemory);
    REJECT_BAIL;

    c->status = napi_get_boolean(env, true, &param);
    REJECT_BAIL;
    videoFlags = frame->videoFrame->GetFlags();
    if ((videoFlags & bmdFrameFlagFlipVertical) != 0) {
      c->status = napi_set_named_property(env, obj, "flipVertical", param);
      REJECT_BAIL;
    }
    if ((videoFlags & bmdFrameHasNoInputSource) != 0) {
      c->status = napi_set_named_property(env, obj, "hasNoInputSource", param);
      REJECT_BAIL;
    }
    if ((videoFlags & bmdFrameCapturedAsPsF) != 0) {
      c->status = napi_set_named_property(env, obj, "capturedAsPsF", param);
      REJECT_BAIL;
    }

    hresult = frame->videoFrame->GetTimecode(bmdTimecodeRP188Any, &timecode);
    switch (hresult) {
      case E_FAIL:
        c->errorMsg = "Unable to access timecode information for video frame.";
        c->status = MACADAM_CALL_FAILURE;
        REJECT_BAIL;
        break;
      case E_ACCESSDENIED:
        c->errorMsg = "An invlid or unsupported timecode format was requested.";
        c->status = MACADAM_ACCESS_DENIED;
        REJECT_BAIL;
        break;
      case S_FALSE:
        c->status = napi_get_boolean(env, false, &param);
        REJECT_BAIL;
        c->status = napi_set_named_property(env, obj, "timecode", param);
        REJECT_BAIL;
        break;
      case S_OK:
        #ifdef WIN32
        BSTR timecodeBSTR = NULL;
        hresult = timecode->GetString(&timecodeBSTR);
        if (hresult == S_OK) {
          _bstr_t timecodeString(timecodeBSTR, false);
          if (crts->roughFps > 30) {
            timecodeString += ((timecode->GetFlags() & bmdTimecodeFieldMark) == 0) ? ".0" : ".1";
          }
          c->status = napi_create_string_utf8(env, (char*) timecodeString, NAPI_AUTO_LENGTH, &param);
          REJECT_BAIL;
        }
        #elif __APPLE__
        CFStringRef timecodeCFString = NULL;
        hresult = timecode->GetString(&timecodeCFString);
        if (hresult == S_OK) {
          char timecodeString[64];
          CFStringGetCString(timecodeCFString, timecodeString, sizeof(timecodeString), kCFStringEncodingMacRoman);
          CFRelease(timecodeCFString);
          if (crts->roughFps > 30) {
            timecodeString[11] = '.';
            timecodeString[12] = ((timecode->GetFlags() & bmdTimecodeFieldMark) == 0) ? '0' : '1';
            timecodeString[13] = '\0';
          }
          c->status = napi_create_string_utf8(env, timecodeString, NAPI_AUTO_LENGTH, &param);
          REJECT_BAIL;
        }
        #else
        char* timecodeString;
        hresult = timecode->GetString((const char **) &timecodeString);
        char tcstr[14];
        for ( uint32_t x = 0; x < 12 ; x++) tcstr[x] = timecodeString[x];
        free(timecodeString);
        if (hresult == S_OK) {
          if (crts->roughFps > 30) {
            tcstr[11] = '.';
            tcstr[12] = ((timecode->GetFlags() & bmdTimecodeFieldMark) == 0) ? '0' : '1';
            tcstr[13] = '\0';
          }
          c->status = napi_create_string_utf8(env, tcstr, NAPI_AUTO_LENGTH, &param);
          REJECT_BAIL;
        }
        #endif

        c->status = napi_set_named_property(env, obj, "timecode", param);
        REJECT_BAIL;

        hresult = timecode->GetTimecodeUserBits(&userBits);
        if (hresult == S_OK) {
          c->status = napi_create_uint32(env, userBits, &param);
          REJECT_BAIL;
        }
        else {
          c->status = napi_get_undefined(env, &param);
          REJECT_BAIL;
        }
        c->status = napi_set_named_property(env, obj, "userbits", param);
        REJECT_BAIL;
        break;
    } // switch GetTimecode

    hresult = frame->videoFrame->GetHardwareReferenceTimestamp(crts->timeScale,
      &frameTime, &frameDuration);
    if (hresult == S_OK) {
      c->status = napi_create_int64(env, frameTime, &param);
      REJECT_BAIL;
      c->status = napi_set_named_property(env, obj, "hardwareRefFrameTime", param);
      REJECT_BAIL;
      c->status = napi_create_int32(env, frameDuration, &param);
      REJECT_BAIL;
      c->status = napi_set_named_property(env, obj, "hardwareRefFrameDuration", param);
      REJECT_BAIL;
    }

    if (frame->audioPacket != nullptr) {
      c->status = napi_create_object(env, &obj);
      REJECT_BAIL;
      c->status = napi_set_named_property(env, result, "audio", obj);
      REJECT_BAIL;
      c->status = napi_create_string_utf8(env, "audioPacket", NAPI_AUTO_LENGTH, &param);
      REJECT_BAIL;
      c->status = napi_set_named_property(env, obj, "type", param);
      REJECT_BAIL;

      hresult = frame->audioPacket->GetPacketTime(&packetTime, crts->sampleRate);
      if (hresult == S_OK) {
        c->status = napi_create_int64(env, packetTime, &param);
        REJECT_BAIL;
        c->status = napi_set_named_property(env, obj, "packetTime", param);
        REJECT_BAIL;
      }

      sampleFrameCount = frame->audioPacket->GetSampleFrameCount();
      c->status = napi_create_int32(env, sampleFrameCount, &param);
      REJECT_BAIL;
      c->status = napi_set_named_property(env, obj, "sampleFrameCount", param);
      REJECT_BAIL;

      sampleFrameCount = frame->audioPacket->GetSampleFrameCount();
      c->status = napi_create_int32(env, sampleFrameCount, &param);
      REJECT_BAIL;
      c->status = napi_set_named_property(env, obj, "sampleFrameCount", param);
      REJECT_BAIL;

      hresult = frame->audioPacket->GetBytes(&bytes);
      if (hresult != S_OK) {
        c->errorMsg = "Failed to access the byte buffer of an audio packet.";
        c->status = MACADAM_CALL_FAILURE;
        REJECT_BAIL;
      }
      audioFinalizeData = (audioData*) malloc(sizeof(audioData));
      audioFinalizeData->audioPacket = frame->audioPacket;
      audioFinalizeData->dataSize = sampleFrameCount * crts->sampleByteFactor;
      c->status = napi_create_external_buffer(env,
        audioFinalizeData->dataSize, bytes, finalizeAudioPacket, audioFinalizeData, &param);
      REJECT_BAIL;
      c->status = napi_set_named_property(env, obj, "data", param);
      REJECT_BAIL;
      c->status = napi_adjust_external_memory(env,
        audioFinalizeData->dataSize, &externalMemory);
      // printf("External memory %li\n", externalMemory);
      REJECT_BAIL;
    }

    c->status = napi_resolve_deferred(env, c->_deferred, result);
    REJECT_BAIL;
    tidyCarrier(env, c);
  }
  else {
    printf("DEBUG: No promise to receive frame.\n");
  }

bail:
  if (!crts->framePromises.empty()) crts->framePromises.pop();
  free(frame);

  return;
}

void captureTsFnFinalize(napi_env env, void* data, void* hint) {
  /* printf("Threadsafe capture finalizer called with data %p and hint %p.\n", data, hint);
  captureThreadsafe* cpts = (captureThreadsafe*) hint;
  delete cpts; */
}
