#include "AgilentFTIR.hpp"
#include <FTIRInst.h>
#include "ParseSettings.hpp"
#include "schneide_base/Logger.hpp"

using namespace vera;

namespace
{
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;
}

_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;
}

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 "";
  }
}

std::string 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 fmt::format("Unknown rejection reason: {0}", static_cast<long>(s));
  }
}

std::string ftirInitError(long errorCode)
{
  switch (errorCode)
  {
    case -1:
      return "(-1) Internal error";
    case -2:
      return "(-2) Cannot connect to the instrument. The instrument is "
             "probably off or disconnected.";
    default:
      return fmt::format("Unknown initialization error: {0}", errorCode);
  }
}
void waitForDoneOrReject()
{
  do
  {
    auto taskProgress = getProgress();
    std::this_thread::sleep_for(std::chrono::milliseconds(250));

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

    if (taskProgress.rejectReason != RR_GOOD)
    {
      auto killed = FTIRInst_KillCollection();
      Logger::get()->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);
}

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

    Logger::get()->info("FTIR scan status: {0} - Work {1} / {2} ",
      ftirStateString(taskProgress.state), taskProgress.currentUnits,
      taskProgress.totalUnits);

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

    if (taskProgress.state != FTIR_Collecting)
    {
      if (taskProgress.state == FTIR_DataReady)
        break;

      throw std::runtime_error(fmt::format(
        "Scan ended with status {0}", ftirStateString(taskProgress.state)));
    }

  } while (true);
}

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

  waitForDoneOrReject();
}

Ptr<Spectrum> singleBeam(long numberOfScans, double from, double to,
  int resolution, bool setBackground)
{
  auto taskProgress = getProgress();
  long const setClean = 0;

  auto started = FTIRInst_dptrStartSingleBeam(numberOfScans, &from, &to,
    static_cast<long>(resolution), setBackground ? 1 : 0, setClean);

  if (started <= 0)
    failure("Unable to start recording 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::get()->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::get()->info(
    "Scanned single-beam from wn {0} to wn {1}, with resolution {2}.",
    actualFrom, actualTo, actualResolution);

  return std::make_shared<Spectrum>(
    Interval(actualFrom, actualTo), std::move(data));
}

Ptr<Spectrum> spectrum(
  long numberOfScans, double from, double to, int resolution)
{
  auto started = FTIRInst_dptrStartSpectrum(numberOfScans, &from, &to,
    static_cast<long>(resolution), XT_WN, YT_Intensity, 0);

  if (started <= 0)
    failure("Unable to start recording 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::get()->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::get()->info(
    "Scanned spectrum from wn {0} to wn {1}, with resolution {2}.", actualFrom,
    actualTo, actualResolution);

  return std::make_shared<Spectrum>(
    Interval(actualFrom, actualTo), std::move(data));
}

bool contains(std::initializer_list<int> list, int value)
{
  for (auto x : list)
    if (x == value)
      return true;
  return false;
}

PHASEPOINTS validatePhasePoints(int phasePoints)
{
  if (!contains({128, 256, 512, 1024}, phasePoints))
    throw std::invalid_argument(
      fmt::format("Invalid phase points setting {0}", phasePoints));
  return static_cast<PHASEPOINTS>(phasePoints);
}

}  // namespace

AgilentFTIR::AgilentFTIR(FTIRConfiguration const& config)
: AgilentFTIR(config.numberOfScans(), config.from(), config.to(),
    config.resolution(), config.recordSpectrums(),
    config.backgroundNumberOfScans(), config.phasePoints(),
    parseApodization(config.phaseApodization()),
    parseApodization(config.interferogramApodization()),
    parseZeroFillingFactor(config.zeroFillingFactor()),
    parseOffsetCorrectionType(config.offsetCorrectionType()))
{
}

AgilentFTIR::AgilentFTIR(int numberOfScans, double from, double to,
  int resolution, bool recordSpectrums, int spectrumBackgroundScans,
  int phasePoints, APODTYPE phaseApodization, APODTYPE interferogramApodization,
  ZFFTYPE zff, OFFSETCORRECTTYPE offset)
: mNumberOfScans(numberOfScans)
, mFrom(from)
, mTo(to)
, mResolution(resolution)
, mRecordSpectrums(recordSpectrums)
, mSpectrumBackgroundScans(spectrumBackgroundScans)
{
  auto const initialized = FTIRInst_Init();
  if (initialized < 0)
  {
    failure(
      "Unable to initialize Agilent FTIR: {0}", ftirInitError(initialized));
  }
  Logger::get()->info("Initialized Agilent FTIR: {0}", initialized);

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

  auto pp = validatePhasePoints(phasePoints);

  FTIRInst_SetComputeParams(pp, PHASETYPE::PT_MERTZ, phaseApodization,
    interferogramApodization, zff, offset);
}

AgilentFTIR::~AgilentFTIR()
{
  FTIRInst_Deinit();
}

void AgilentFTIR::prepare(Interactor& interactor)
{
  calibrate(interactor);

  if (mRecordSpectrums)
  {
    interactor.say("Starting background scan...");
    // Do a background scan
    singleBeam(mSpectrumBackgroundScans, mFrom, mTo, mResolution, true);
  }
}

Ptr<Spectrum> AgilentFTIR::scan(Interactor& interactor)
{
  if (mRecordSpectrums)
  {
    // Now to the real scan
    return spectrum(mNumberOfScans, mFrom, mTo, mResolution);
  }
  return singleBeam(mNumberOfScans, mFrom, mTo, mResolution, false);
}

void AgilentFTIR::calibrate(Interactor& interactor)
{
  auto constexpr STORE_VOLATILE = 0;
  auto constexpr STORE_NON_VOLATILE = 1;
  auto constexpr RESTORE_FACTORY = -1;
  long constexpr LASER_ENERGY_MAXIMUM = 32768;
  long constexpr LASER_ENERGY_OPTIMUM = 25000;
  
  long best = -1;
  long bestGain = -1;
  long firstEnergy = -1;

  for (std::size_t i = 0; i < 16; ++i)
  {
    auto gain = getGain();
    testBeam();
    auto status = getStatus();
    auto energy = status.nEnergyStatus;
    
    Logger::get()->info("Gain: {1}, Energy status: {0}", energy, gain);

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

        bestGain = gain;
        best = energy;
        
      }
      else
      {
        interactor.say("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);
    }
  }
}
