
#include <spdlog/spdlog.h>

#include <spdlog/sinks/stdout_sinks.h>
#include <fstream>
#include <memory>
#include "FTIRInst.h"

std::shared_ptr<spdlog::logger> const& logger()
{
  static std::shared_ptr<spdlog::logger> logger;
  if (!logger)
    logger = spdlog::stdout_logger_mt("console");
  return logger;
}

template <class... P>
void failure(char const* format, P&&... p)
{
  throw std::runtime_error(fmt::format(format, std::forward<P>(p)...));
}

_progress getProgress()
{
  _progress taskProgress{};
  taskProgress.nStructSize = sizeof(_progress);
  auto progressSize = FTIRInst_CheckProgressStruct(&taskProgress);
  return taskProgress;
}

const char* ftirStateString(FTIR_STATE s)
{
  switch (s)
  {
    case FTIR_Init:
      return "Init";
    case FTIR_Collecting:
      return "Collecting";
    case FTIR_DataReady:
      return "Data ready";
    case FTIR_Aborting:
      return "Aborting";
    case FTIR_Error:
      return "Error";
    default:
      return "";
  }
}

const char* ftirRejectReason(REJECTREASON s)
{
  switch (s)
  {
    case RR_GOOD:
      return "Good";
    case RR_20PCT:
      return "20PCT";
    case RR_CENTERBURST:
      return "Centerburst";
    case RR_HW_UNSTABLE:
      return "Hardware unstable";
    default:
      return "";
  }
}

void waitForDone()
{
  do
  {
    auto taskProgress = getProgress();
    std::this_thread::sleep_for(std::chrono::milliseconds(250));

    logger()->info("Status: {0} - Work {1} / {2} ",
      ftirStateString(taskProgress.state), taskProgress.currentUnits,
      taskProgress.totalUnits);

    if (taskProgress.rejectReason != RR_GOOD)
      logger()->warn("Rejected because {0}",
        ftirRejectReason(static_cast<REJECTREASON>(taskProgress.rejectReason)));

    if (taskProgress.state != FTIR_Collecting)
      break;

  } while (true);
}

void waitForDoneOrReject()
{
  do
  {
    auto taskProgress = getProgress();
    std::this_thread::sleep_for(std::chrono::milliseconds(250));

    logger()->info("Status: {0} - Work {1} / {2} ",
      ftirStateString(taskProgress.state), taskProgress.currentUnits,
      taskProgress.totalUnits);

    if (taskProgress.rejectReason != RR_GOOD)
    {
      auto killed = FTIRInst_KillCollection();
      logger()->warn("Rejected because {0}, killed: {1}",
        ftirRejectReason(static_cast<REJECTREASON>(taskProgress.rejectReason)),
        killed);

      if (killed == 0)
        break;
    }
    if (taskProgress.state != FTIR_Collecting)
      break;

  } while (true);
}

std::vector<double> backgroundBeam(double from, double to)
{
  auto taskProgress = getProgress();
  long const setBackground = 1;
  long const setClean = 0;
  auto started =
    FTIRInst_dptrStartSingleBeam(32, &from, &to, 8, setBackground, setClean);

  if (started <= 0)
    failure("Unable to start single beam: {0}", started);

  waitForDone();

  double actualFrom = 0.0;
  double actualTo = 0.0;
  long actualResolution = 0;
  auto bufferLength = FTIRInst_dptrGetSingleBeam(
    nullptr, 0, &actualFrom, &actualTo, &actualResolution);
  logger()->info("Got buffer length: {0}", bufferLength);
  if (bufferLength <= 0)
    failure("Invalid buffer size");

  std::vector<double> data(bufferLength);

  auto bufferLength2 = FTIRInst_dptrGetSingleBeam(
    data.data(), data.size(), &actualFrom, &actualTo, &actualResolution);
  logger()->info("Scanned from {0} to {1}.", actualFrom, actualTo);
  return data;
}

std::vector<double> spectrum(double from, double to)
{
  long const setBackground = 1;
  long const setClean = 0;
  auto started =
    FTIRInst_dptrStartSpectrum(32, &from, &to, 8, XT_WN, YT_Intensity, 0);

  if (started <= 0)
    failure("Unable to start spectrum: {0}", started);

  waitForDone();

  double actualFrom = 0.0;
  double actualTo = 0.0;
  long actualResolution = 0;
  auto bufferLength = FTIRInst_dptrGetSpectrum(
    nullptr, 0, &actualFrom, &actualTo, &actualResolution);
  logger()->info("Got buffer length: {0}", bufferLength);
  if (bufferLength <= 0)
    failure("Invalid buffer size");

  std::vector<double> data(bufferLength);

  auto bufferLength2 = FTIRInst_dptrGetSpectrum(
    data.data(), data.size(), &actualFrom, &actualTo, &actualResolution);
  logger()->info("Scanned spectrum from {0} to {1}.", actualFrom, actualTo);

  std::ofstream file("D:/scan.csv", std::ios::trunc);
  int index = 0;
  for (auto const& each : data)
  {
    auto x = static_cast<double>(index) / (data.size() - 1);
    x = actualFrom + x * (actualTo - actualFrom);
    file << x << "," << each << std::endl;
    ++index;
  }
  return data;
}

void testBeam()
{
  auto started = FTIRInst_StartCoaddedIgram(1, 8, 128);
  if (started < 0)
    failure("Unable to recording: {0}", started);

  waitForDoneOrReject();
}

template <typename... Args>
void print(Args&&... args)
{
  logger()->info(std::forward<Args>(args)...);
}

auto constexpr LASER_ENERGY_MAX = 29000;
auto constexpr LASER_ENERGY_MIN = 15000;
long constexpr LASER_ENERGY_OPTIMUM = 25000;

_instrumentMLDiag getStatus()
{
  _instrumentMLDiag status;
  std::memset(&status, 0, sizeof(_instrumentMLDiag));
  status.nVersion = 102;
  FTIRInst_GetStatusEx(&status);
  return status;
}

long getGain()
{
  long gain = 0;
  auto error = FTIRInst_GetIrGain(&gain);
  if (error != 0)
    throw std::runtime_error(
      "Unable to get gain value: " + std::to_string(error));
  return gain;
}

void calibrate(int wait)
{
  auto constexpr STORE_VOLATILE = 0;
  auto constexpr STORE_NON_VOLATILE = 1;
  auto constexpr RESTORE_FACTORY = -1;
  long best = -1;
  long bestGain = -1;
  long firstEnergy = -1;
  
  // Reset to factory
  FTIRInst_SetIrGain(RESTORE_FACTORY, STORE_VOLATILE);

  for (std::size_t i = 0; i < 16; ++i)
  {
    auto gain = getGain();

    if (wait)
      std::this_thread::sleep_for(std::chrono::milliseconds(wait));

    testBeam();
    auto status = getStatus();
    auto energy = status.nEnergyStatus;
    
    print("Gain: {1}, Energy status: {0}", energy, gain);

    {
      if (bestGain == -1 || std::abs(energy-LASER_ENERGY_OPTIMUM) < std::abs(best-LASER_ENERGY_OPTIMUM))
      {
        if (bestGain == -1)
          firstEnergy = energy;

        bestGain = gain;
        best = energy;
        
      }
      else
      {
        print("Setting gain to {0}", bestGain);
        FTIRInst_SetIrGain(bestGain, STORE_NON_VOLATILE);
        break;
      }
    }

    if (firstEnergy > LASER_ENERGY_OPTIMUM)
    {
      FTIRInst_SetIrGain(gain-1, STORE_VOLATILE);
    }
    else
    {
      FTIRInst_SetIrGain(gain+1, STORE_VOLATILE);
    }
  }
}

void printStatus()
{
  _instrumentMLDiag status;
  std::memset(&status, 0, sizeof(_instrumentMLDiag));
  status.nVersion = 102;
  FTIRInst_GetStatusEx(&status);

  print("Energy Status: {0}", status.nEnergyStatus);
  print("Laser Status: {0}", status.nLaserStatus);
  print("NumTemps: {0}", status.numTemps);

  print("Source Current Status: {0}", status.fSourceCurrentStatus);
  print("Source Voltage Status: {0}", status.fSourceVoltageStatus);

  print("Temp CPU: {0}", status.fTempCPU);
  print("Temp Power: {0}", status.fTempPower);
  print("Temp IR: {0}", status.fTempIR);
  print("Temp Detector: {0}", status.fTempDetector);

  MLSyncdVals vals;
  std::memset(&vals, 0, sizeof(MLSyncdVals));
  vals.nVersion = 101;
  print("Block temp valid: {0}", vals.bBlockTempValid);
  print("Block temp: {0}", vals.fBlockTemp);
  print("Adjusted laser freq: {0}", vals.fAdjustedLaserFreq);
  print("Adjust enabled: {0}", vals.nAdjustEnabled);
  print("Laser Standardization Type: {0}", vals.nLaserStandardizationType);
}

class FTIR
{
public:
  FTIR()
  {
    auto initialized = FTIRInst_Init();
    logger()->info("Initialized: {0}", initialized);
    if (initialized < 0)
    {
      failure("Unable to initialize: {0}", initialized);
    }
  }

  FTIR(FTIR const&) = delete;

  ~FTIR()
  {
    FTIRInst_Deinit();
  }

  FTIR& operator=(FTIR const&) = delete;
};

void run(int argc, char** argv)
{
  auto target = FTIRInst_SetTargetDeviceUsb(nullptr);
  logger()->info("Target {0}", target);

  FTIR ftir;

  _instrumentMLVersion versionInfo;
  std::memset(&versionInfo, 0, sizeof(_instrumentMLVersion));
  versionInfo.nVersion = 103;
  FTIRInst_GetVersionEx(&versionInfo);
  logger()->info(
    "Versions: DLL {0}, Firmware {1} ", versionInfo.dllRev, versionInfo.fwRev);

  FTIRInst_SetComputeParams(PHASEPOINTS::PP_128, PHASETYPE::PT_MERTZ,
    APODTYPE::APOD_NONE, APODTYPE::APOD_NONE, ZFFTYPE::ZFF_NONE,
    OFFSETCORRECTTYPE::OT_NONE);
  
  int wait = 0;
  
  if (argc > 1 && std::string(argv[1]) == "-s")
    wait = std::stol(argv[1]);

  calibrate(wait);

  // double from = 4000;
  // double to = 400;
  // backgroundBeam(from, to);

  // spectrum(from, to);

  // FTIRInst_dptrStartSpectrum(32, )
}

int main(int argc, char** argv)
{
  try
  {
    run(argc, argv);
    system("pause");
    return 0;
  }
  catch (std::exception const& e)
  {
    logger()->error("Error: {0}", e.what());
    system("pause");
    return -1;
  }
}