// Copyright (c) 2012-2017 VideoStitch SAS
// Copyright (c) 2018 stitchEm

#include "ntv2Discovery.hpp"

#include "libvideostitch/logging.hpp"

#include <thread>
#include <future>
#include <chrono>
#include <algorithm>
#include <locale>
#include <codecvt>

#include <ntv2utils.h>

using namespace VideoStitch;
using namespace Plugin;

Ntv2Discovery* Ntv2Discovery::create() {
  std::vector<std::string> cards;
  std::vector<std::shared_ptr<Device>> devices;

  CNTV2DeviceScanner ajaDeviceScanner;
  ajaDeviceScanner.ScanHardware();
  size_t nbCard = ajaDeviceScanner.GetNumDevices();
  if (nbCard == 0) return nullptr;

  for (uint32_t iDevice = 0; iDevice < nbCard; ++iDevice) {
    Device device;
    device.boardInfo = ajaDeviceScanner.GetDeviceInfoList()[iDevice];
    CNTV2Card card;
    CNTV2DeviceScanner::GetDeviceAtIndex(iDevice, card);
    cards.push_back(device.boardInfo.deviceIdentifier);

    for (int8_t i = 0; i < device.boardInfo.numVidInputs; ++i) {
      std::shared_ptr<InputDevice> inputDevice = std::make_shared<InputDevice>();
      // Aja inputs are labeled from 1 to numVidInputs
      inputDevice->pluginDevice.displayName = device.boardInfo.deviceIdentifier + " Input " + std::to_string(i + 1);
      inputDevice->pluginDevice.name = std::to_string(device.boardInfo.deviceIndex) + std::to_string(i);
      inputDevice->pluginDevice.type = Plugin::DiscoveryDevice::CAPTURE;
      inputDevice->pluginDevice.mediaType = Plugin::DiscoveryDevice::MediaType::AUDIO_AND_VIDEO;
      inputDevice->boardIdx = device.boardInfo.deviceIndex;
      inputDevice->channelIdx = i;
      inputDevice->boardInfo = device.boardInfo;
      devices.push_back(inputDevice);
    }

    for (int8_t i = 0; i < device.boardInfo.numVidOutputs; ++i) {
      std::shared_ptr<OutputDevice> outputDevice = std::make_shared<OutputDevice>();
      // Aja outputs are labeled from 1 to numVidOutputs
      outputDevice->pluginDevice.displayName = device.boardInfo.deviceIdentifier + " Output " + std::to_string(i + 1);
      outputDevice->pluginDevice.name = std::to_string(device.boardInfo.deviceIndex) + std::to_string(i);
      outputDevice->pluginDevice.type = Plugin::DiscoveryDevice::PLAYBACK;
      outputDevice->pluginDevice.mediaType = Plugin::DiscoveryDevice::MediaType::AUDIO_AND_VIDEO;
      outputDevice->boardIdx = device.boardInfo.deviceIndex;
      outputDevice->channelIdx = i;
      outputDevice->boardInfo = device.boardInfo;
      devices.push_back(outputDevice);
    }
  }
  return new Ntv2Discovery(cards, devices);
}

Ntv2Discovery::Ntv2Discovery(const std::vector<std::string>& cards, const std::vector<std::shared_ptr<Device>>& devices)
    : m_cards(cards), m_devices(devices) {}

Ntv2Discovery::~Ntv2Discovery() {}

std::string Ntv2Discovery::name() const { return "aja"; }

std::string Ntv2Discovery::readableName() const { return "AJA"; }

std::vector<Plugin::DiscoveryDevice> Ntv2Discovery::inputDevices() {
  std::vector<Plugin::DiscoveryDevice> pluginDevices;

  for (auto it = m_devices.begin(); it != m_devices.end(); ++it) {
    if ((*it)->pluginDevice.type == Plugin::DiscoveryDevice::CAPTURE) {
      pluginDevices.push_back((*it)->pluginDevice);
    }
  }

  return pluginDevices;
}

std::vector<Plugin::DiscoveryDevice> Ntv2Discovery::outputDevices() {
  std::vector<Plugin::DiscoveryDevice> pluginDevices;

  for (auto it = m_devices.begin(); it != m_devices.end(); ++it) {
    if ((*it)->pluginDevice.type == Plugin::DiscoveryDevice::PLAYBACK) {
      pluginDevices.push_back((*it)->pluginDevice);
    }
  }

  return pluginDevices;
}

std::vector<string> Ntv2Discovery::cards() const { return m_cards; }

void Ntv2Discovery::registerAutoDetectionCallback(AutoDetection& autoDetection) {
  return;  // Incompatible with AJA Reader in its actual form, need a wrapper of the AJA SDK
}

DisplayMode Ntv2Discovery::currentDisplayMode(const DiscoveryDevice& device) {
  const auto it = std::find_if(
      m_devices.begin(), m_devices.end(),
      [&device](const std::shared_ptr<Device>& m_device) -> bool { return device == m_device->pluginDevice; });
  if (it != m_devices.end()) {
    CNTV2Card card;
    if (!CNTV2DeviceScanner::GetDeviceAtIndex((*it)->boardIdx, card)) {
      return DisplayMode(0, 0, false, {1, 1});
    }

    const NTV2InputSource inputSource = GetNTV2InputSourceForIndex((*it)->channelIdx);
    if (inputSource == NTV2_INPUTSOURCE_INVALID) {
      return DisplayMode(0, 0, false, {1, 1});
    }

    const NTV2VideoFormat videoFormat = card.GetInputVideoFormat(inputSource);
    return aja2vsDisplayFormat(videoFormat);
  }
  return DisplayMode(0, 0, false, {1, 1});
}

std::vector<DisplayMode> Ntv2Discovery::supportedDisplayModes(const Plugin::DiscoveryDevice& device) {
  auto it = std::find_if(
      m_devices.begin(), m_devices.end(),
      [&device](const std::shared_ptr<Device>& m_device) -> bool { return device == m_device->pluginDevice; });
  if (it != m_devices.end()) {
    std::vector<DisplayMode> supportedDisplayModes;
    CNTV2Card card;
    CNTV2DeviceScanner::GetDeviceAtIndex((*it)->boardIdx, card);
    NTV2VideoFormatSet outFormats;
    card.GetSupportedVideoFormats(outFormats);

    for (auto it = outFormats.begin(); it != outFormats.end(); ++it) {
      const DisplayMode displayMode = aja2vsDisplayFormat((*it));
      if (displayMode.width != 0 && displayMode.height != 0) {
        supportedDisplayModes.push_back(displayMode);
      }
    }
    std::sort(supportedDisplayModes.begin(), supportedDisplayModes.end());
    return supportedDisplayModes;
  } else {
    return std::vector<DisplayMode>();
  }
}

std::vector<PixelFormat> Ntv2Discovery::supportedPixelFormat(const Plugin::DiscoveryDevice& device) {
  auto it = std::find_if(
      m_devices.begin(), m_devices.end(),
      [&device](const std::shared_ptr<Device>& m_device) -> bool { return device == m_device->pluginDevice; });
  if (it != m_devices.end()) {
    std::vector<PixelFormat> pixelFormats;
    PixelFormat vsPF;
    // iterate on enum with defined values following themselves : check ntv2enums.h
    for (uint32_t i = NTV2_FBF_10BIT_YCBCR; i < NTV2_FBF_NUMFRAMEBUFFERFORMATS; ++i) {
      if (NTV2DeviceCanDoFrameBufferFormat((*it)->boardInfo.deviceID, (NTV2FrameBufferFormat)i)) {
        // convertPixelFormat((NTV2FrameBufferFormat)i, vsPF);
        vsPF = aja2vsPixelFormat((NTV2FrameBufferFormat)i);
        if (vsPF != Unknown) pixelFormats.push_back(vsPF);
      }
    }
    return pixelFormats;
  } else {
    return std::vector<PixelFormat>();
  }
}

std::vector<int> Ntv2Discovery::supportedNbChannels(const Plugin::DiscoveryDevice& /*device*/) {
  std::vector<int> channels;
  CNTV2DeviceScanner ajaDeviceScanner;
  ajaDeviceScanner.ScanHardware();

  for (uint32_t iDevice = 0; iDevice < ajaDeviceScanner.GetNumDevices(); ++iDevice) {
    channels.push_back((int)ajaDeviceScanner.GetDeviceInfoList()[iDevice].numAudioStreams);
  }

  return channels;
}

std::vector<Audio::SamplingRate> Ntv2Discovery::supportedSamplingRates(const Plugin::DiscoveryDevice& /*device*/) {
  std::vector<Audio::SamplingRate> rates;
  CNTV2DeviceScanner ajaDeviceScanner;
  ajaDeviceScanner.ScanHardware();

  for (uint32_t iDevice = 0; iDevice < ajaDeviceScanner.GetNumDevices(); ++iDevice) {
    NTV2AudioSampleRateList audioSampleRateList = ajaDeviceScanner.GetDeviceInfoList()[iDevice].audioSampleRateList;
    for (auto it = audioSampleRateList.begin(); it != audioSampleRateList.end(); it++) {
      Audio::SamplingRate sRate = convertSamplerate(*it);
      if (sRate != Audio::SamplingRate::SR_NONE) rates.push_back(sRate);
    }
  }

  return rates;
}

std::vector<Audio::SamplingDepth> Ntv2Discovery::supportedSampleFormats(const Plugin::DiscoveryDevice& /*device*/) {
  std::vector<Audio::SamplingDepth> formats;
  CNTV2DeviceScanner ajaDeviceScanner;
  ajaDeviceScanner.ScanHardware();

  for (uint32_t iDevice = 0; iDevice < ajaDeviceScanner.GetNumDevices(); ++iDevice) {
    NTV2AudioBitsPerSampleList audioBitsPerSampleList =
        ajaDeviceScanner.GetDeviceInfoList()[iDevice].audioBitsPerSampleList;
    for (auto it = audioBitsPerSampleList.begin(); it != audioBitsPerSampleList.end(); it++) {
      Audio::SamplingDepth sDepth = convertFormats(*it);
      if (sDepth != Audio::SamplingDepth::SD_NONE) formats.push_back(sDepth);
    }
  }

  return formats;
}

Audio::SamplingRate Ntv2Discovery::convertSamplerate(AudioSampleRateEnum ntv2SampleRate) {
  switch (ntv2SampleRate) {
    case k44p1KHzSampleRate:
      return Audio::SamplingRate::SR_44100;
    case k48KHzSampleRate:
      return Audio::SamplingRate::SR_48000;
    case k96KHzSampleRate:
      return Audio::SamplingRate::SR_96000;
    default:
      return Audio::SamplingRate::SR_NONE;
  }
}

Audio::SamplingDepth Ntv2Discovery::convertFormats(AudioBitsPerSampleEnum ntv2Format) {
  switch (ntv2Format) {
    case k16bitsPerSample:
      return Audio::SamplingDepth::INT16;
    case k24bitsPerSample:
      return Audio::SamplingDepth::INT32;  // 24bits audio PCM are stored in 32bits
      break;
    case k32bitsPerSample:
      return Audio::SamplingDepth::INT32;
    default:
      return Audio::SamplingDepth::SD_NONE;
  }
}