#pragma once

#include "../Utils/Library.h"

class MaestroLibrary : public Utils::Library {
 public:
  MaestroLibrary(const MaestroLibrary&) = delete;
  MaestroLibrary& operator=(const MaestroLibrary&) = delete;

  MaestroLibrary(MaestroLibrary&&) = default;
  MaestroLibrary& operator=(MaestroLibrary&&) = default;

  MaestroLibrary() noexcept {}

  virtual ~MaestroLibrary() {}

  bool Init(const char* libName) noexcept override {
    if (Utils::Library::Init(libName)) {
      fGetMaestroObject = (void* (*)())GetFunction("GetMaestroObjectWithMute");
      CheckFunction((void*)fGetMaestroObject, __LINE__);
      if (fGetMaestroObject) {
        maestro = fGetMaestroObject();
        if (maestro) {
          fCreateSimpleSimulator =
              (unsigned long int (*)(int))GetFunction("CreateSimpleSimulator");
          CheckFunction((void*)fCreateSimpleSimulator, __LINE__);
          fDestroySimpleSimulator = (void (*)(unsigned long int))GetFunction(
              "DestroySimpleSimulator");
          CheckFunction((void*)fDestroySimpleSimulator, __LINE__);

          fRemoveAllOptimizationSimulatorsAndAdd = (int (*)(
              unsigned long int simHandle, int simType, int simExecType))
              GetFunction("RemoveAllOptimizationSimulatorsAndAdd");
          CheckFunction((void*)fRemoveAllOptimizationSimulatorsAndAdd,
                        __LINE__);
          fAddOptimizationSimulator =
              (int (*)(unsigned long int simHandle, int simType,
                       int simExecType))GetFunction("AddOptimizationSimulator");
          CheckFunction((void*)fAddOptimizationSimulator, __LINE__);

          fSimpleExecute = (char* (*)(unsigned long int, const char*,
                                      const char*))GetFunction("SimpleExecute");
          CheckFunction((void*)fSimpleExecute, __LINE__);

          fSimpleEstimate = (char* (*)(unsigned long int, const char*, const char*,
                                       const char*))GetFunction("SimpleEstimate");
          CheckFunction((void*)fSimpleEstimate, __LINE__);

          fFreeResult = (void (*)(char*))GetFunction("FreeResult");
          CheckFunction((void*)fFreeResult, __LINE__);

          fCreateSimulator =
              (unsigned long int (*)(int, int))GetFunction("CreateSimulator");
          CheckFunction((void*)fCreateSimulator, __LINE__);
          fGetSimulator =
              (void* (*)(unsigned long int))GetFunction("GetSimulator");
          CheckFunction((void*)fGetSimulator, __LINE__);
          fDestroySimulator =
              (void (*)(unsigned long int))GetFunction("DestroySimulator");
          CheckFunction((void*)fDestroySimulator, __LINE__);

          fInitializeSimulator =
              (int (*)(void*))GetFunction("InitializeSimulator");
          CheckFunction((void*)fInitializeSimulator, __LINE__);
          fResetSimulator = (int (*)(void*))GetFunction("ResetSimulator");
          CheckFunction((void*)fResetSimulator, __LINE__);
          fConfigureSimulator =
              (int (*)(void*, const char*, const char*))GetFunction(
                  "ConfigureSimulator");
          CheckFunction((void*)fConfigureSimulator, __LINE__);
          fGetConfiguration =
              (char* (*)(void*, const char*))GetFunction("GetConfiguration");
          CheckFunction((void*)fGetConfiguration, __LINE__);
          fAllocateQubits = (unsigned long int (*)(
              void*, unsigned long int))GetFunction("AllocateQubits");
          CheckFunction((void*)fAllocateQubits, __LINE__);
          fGetNumberOfQubits =
              (unsigned long int (*)(void*))GetFunction("GetNumberOfQubits");
          CheckFunction((void*)fGetNumberOfQubits, __LINE__);
          fClearSimulator = (int (*)(void*))GetFunction("ClearSimulator");
          CheckFunction((void*)fClearSimulator, __LINE__);
          fMeasure = (unsigned long long int (*)(
              void*, const unsigned long int*,
              unsigned long int))GetFunction("Measure");
          CheckFunction((void*)fMeasure, __LINE__);
          fApplyReset = (int (*)(void*, const unsigned long int*,
                                 unsigned long int))GetFunction("ApplyReset");
          CheckFunction((void*)fApplyReset, __LINE__);
          fProbability = (double (*)(void*, unsigned long long int))GetFunction(
              "Probability");
          CheckFunction((void*)fProbability, __LINE__);
          fFreeDoubleVector =
              (void (*)(double*))GetFunction("FreeDoubleVector");
          CheckFunction((void*)fFreeDoubleVector, __LINE__);
          fFreeULLIVector =
              (void (*)(unsigned long long int*))GetFunction("FreeULLIVector");
          CheckFunction((void*)fFreeULLIVector, __LINE__);
          fAmplitude = (double* (*)(void*, unsigned long long int))GetFunction(
              "Amplitude");
          CheckFunction((void*)fAmplitude, __LINE__);
          fAllProbabilities =
              (double* (*)(void*))GetFunction("AllProbabilities");
          CheckFunction((void*)fAllProbabilities, __LINE__);
          fProbabilities =
              (double* (*)(void*, const unsigned long long int*,
                           unsigned long int))GetFunction("Probabilities");
          CheckFunction((void*)fProbabilities, __LINE__);
          fSampleCounts =
              (unsigned long long int* (*)(void*, const unsigned long long int*,
                                           unsigned long int,
                                           unsigned long int))
                  GetFunction("SampleCounts");
          CheckFunction((void*)fSampleCounts, __LINE__);
          fGetSimulatorType = (int (*)(void*))GetFunction("GetSimulatorType");
          CheckFunction((void*)fGetSimulatorType, __LINE__);
          fGetSimulationType = (int (*)(void*))GetFunction("GetSimulationType");
          CheckFunction((void*)fGetSimulationType, __LINE__);
          fFlushSimulator = (int (*)(void*))GetFunction("FlushSimulator");
          CheckFunction((void*)fFlushSimulator, __LINE__);
          fSaveStateToInternalDestructive =
              (int (*)(void*))GetFunction("SaveStateToInternalDestructive");
          CheckFunction((void*)fSaveStateToInternalDestructive, __LINE__);
          fRestoreInternalDestructiveSavedState = (int (*)(void*))GetFunction(
              "RestoreInternalDestructiveSavedState");
          CheckFunction((void*)fRestoreInternalDestructiveSavedState, __LINE__);
          fSaveState = (int (*)(void*))GetFunction("SaveState");
          CheckFunction((void*)fSaveState, __LINE__);
          fRestoreState = (int (*)(void*))GetFunction("RestoreState");
          CheckFunction((void*)fRestoreState, __LINE__);
          fSetMultithreading =
              (int (*)(void*, int))GetFunction("SetMultithreading");
          CheckFunction((void*)fSetMultithreading, __LINE__);
          fGetMultithreading = (int (*)(void*))GetFunction("GetMultithreading");
          CheckFunction((void*)fGetMultithreading, __LINE__);
          fIsQcsim = (int (*)(void*))GetFunction("IsQcsim");
          CheckFunction((void*)fIsQcsim, __LINE__);
          fMeasureNoCollapse = (unsigned long long int (*)(void*))GetFunction(
              "MeasureNoCollapse");
          CheckFunction((void*)fMeasureNoCollapse, __LINE__);

          fApplyX = (int (*)(void*, int))GetFunction("ApplyX");
          CheckFunction((void*)fApplyX, __LINE__);
          fApplyY = (int (*)(void*, int))GetFunction("ApplyY");
          CheckFunction((void*)fApplyY, __LINE__);
          fApplyZ = (int (*)(void*, int))GetFunction("ApplyZ");
          CheckFunction((void*)fApplyZ, __LINE__);
          fApplyH = (int (*)(void*, int))GetFunction("ApplyH");
          CheckFunction((void*)fApplyH, __LINE__);
          fApplyS = (int (*)(void*, int))GetFunction("ApplyS");
          CheckFunction((void*)fApplyS, __LINE__);
          fApplySDG = (int (*)(void*, int))GetFunction("ApplySDG");
          CheckFunction((void*)fApplySDG, __LINE__);
          fApplyT = (int (*)(void*, int))GetFunction("ApplyT");
          CheckFunction((void*)fApplyT, __LINE__);
          fApplyTDG = (int (*)(void*, int))GetFunction("ApplyTDG");
          CheckFunction((void*)fApplyTDG, __LINE__);
          fApplySX = (int (*)(void*, int))GetFunction("ApplySX");
          CheckFunction((void*)fApplySX, __LINE__);
          fApplySXDG = (int (*)(void*, int))GetFunction("ApplySXDG");
          CheckFunction((void*)fApplySXDG, __LINE__);
          fApplyK = (int (*)(void*, int))GetFunction("ApplyK");
          CheckFunction((void*)fApplyK, __LINE__);
          fApplyP = (int (*)(void*, int, double))GetFunction("ApplyP");
          CheckFunction((void*)fApplyP, __LINE__);
          fApplyRx = (int (*)(void*, int, double))GetFunction("ApplyRx");
          CheckFunction((void*)fApplyRx, __LINE__);
          fApplyRy = (int (*)(void*, int, double))GetFunction("ApplyRy");
          CheckFunction((void*)fApplyRy, __LINE__);
          fApplyRz = (int (*)(void*, int, double))GetFunction("ApplyRz");
          CheckFunction((void*)fApplyRz, __LINE__);
          fApplyU = (int (*)(void*, int, double, double, double,
                             double))GetFunction("ApplyU");
          CheckFunction((void*)fApplyU, __LINE__);
          fApplyCX = (int (*)(void*, int, int))GetFunction("ApplyCX");
          CheckFunction((void*)fApplyCX, __LINE__);
          fApplyCY = (int (*)(void*, int, int))GetFunction("ApplyCY");
          CheckFunction((void*)fApplyCY, __LINE__);
          fApplyCZ = (int (*)(void*, int, int))GetFunction("ApplyCZ");
          CheckFunction((void*)fApplyCZ, __LINE__);
          fApplyCH = (int (*)(void*, int, int))GetFunction("ApplyCH");
          CheckFunction((void*)fApplyCH, __LINE__);
          fApplyCSX = (int (*)(void*, int, int))GetFunction("ApplyCSX");
          CheckFunction((void*)fApplyCSX, __LINE__);
          fApplyCSXDG = (int (*)(void*, int, int))GetFunction("ApplyCSXDG");
          CheckFunction((void*)fApplyCSXDG, __LINE__);
          fApplyCP = (int (*)(void*, int, int, double))GetFunction("ApplyCP");
          CheckFunction((void*)fApplyCP, __LINE__);
          fApplyCRx = (int (*)(void*, int, int, double))GetFunction("ApplyCRx");
          CheckFunction((void*)fApplyCRx, __LINE__);
          fApplyCRy = (int (*)(void*, int, int, double))GetFunction("ApplyCRy");
          CheckFunction((void*)fApplyCRy, __LINE__);
          fApplyCRz = (int (*)(void*, int, int, double))GetFunction("ApplyCRz");
          CheckFunction((void*)fApplyCRz, __LINE__);
          fApplyCCX = (int (*)(void*, int, int, int))GetFunction("ApplyCCX");
          CheckFunction((void*)fApplyCCX, __LINE__);
          fApplySwap = (int (*)(void*, int, int))GetFunction("ApplySwap");
          CheckFunction((void*)fApplySwap, __LINE__);
          fApplyCSwap =
              (int (*)(void*, int, int, int))GetFunction("ApplyCSwap");
          CheckFunction((void*)fApplyCSwap, __LINE__);
          fApplyCU = (int (*)(void*, int, int, double, double, double,
                              double))GetFunction("ApplyCU");
          CheckFunction((void*)fApplyCU, __LINE__);

          return true;
        }
      } else
        std::cerr << "MaestroLibrary: Unable to get initialization function "
                     "for library"
                  << std::endl;
    } else
      std::cerr << "MaestroLibrary: Unable to load the library" << std::endl;

    return false;
  }

  static void CheckFunction(void* func, int line) noexcept {
    if (!func) {
      std::cerr << "MaestroLibrary: Unable to load function, line #: " << line;

#ifdef __linux__
      const char* dlsym_error = dlerror();
      if (dlsym_error) std::cerr << ", error: " << dlsym_error;
#elif defined(_WIN32)
      const DWORD error = GetLastError();
      std::cerr << ", error code: " << error;
#endif

      std::cerr << std::endl;
    }
  }

  bool IsValid() const { return maestro != nullptr; }

  virtual unsigned long int CreateSimpleSimulator(int nrQubits) {
    if (maestro && fCreateSimpleSimulator)
      return fCreateSimpleSimulator(nrQubits);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to create the simple simulator.");

    return 0;
  }

  void DestroySimpleSimulator(unsigned long int simHandle) {
    if (maestro && fDestroySimpleSimulator)
      fDestroySimpleSimulator(simHandle);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to destroy the simple simulator.");
  }

  int RemoveAllOptimizationSimulatorsAndAdd(unsigned long int simHandle,
                                            int simType, int simExecType) {
    if (maestro && fRemoveAllOptimizationSimulatorsAndAdd)
      return fRemoveAllOptimizationSimulatorsAndAdd(simHandle, simType,
                                                    simExecType);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to remove all optimization simulators and "
          "add a new one.");

    return 0;
  }

  int AddOptimizationSimulator(unsigned long int simHandle, int simType,
                               int simExecType) {
    if (maestro && fAddOptimizationSimulator)
      return fAddOptimizationSimulator(simHandle, simType, simExecType);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to add an optimization simulator.");

    return 0;
  }

  char* SimpleExecute(unsigned long int simpleSim, const char* jsonCircuit,
                      const char* jsonConfig) {
    if (maestro && fSimpleExecute)
      return fSimpleExecute(simpleSim, jsonCircuit, jsonConfig);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to execute the simple simulator.");

    return nullptr;
  }

  char* SimpleEstimate(unsigned long int simpleSim, const char* jsonCircuit,
                       const char* observableStr, const char* jsonConfig) {
    if (maestro && fSimpleEstimate)
      return fSimpleEstimate(simpleSim, jsonCircuit, observableStr, jsonConfig);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to execute the simple simulator for "
          "expectation values.");

    return nullptr;
  }

  virtual void FreeResult(char* result) {
    if (maestro && fFreeResult)
      fFreeResult(result);
    else
      throw std::runtime_error("MaestroLibrary: Unable to free the result.");
  }

  virtual unsigned long int CreateSimulator(int simType, int simExecType) {
    if (maestro && fCreateSimulator)
      return fCreateSimulator(simType, simExecType);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to create the simulator.");

    return 0;
  }

  void* GetSimulator(unsigned long int simHandle) {
    if (maestro && fGetSimulator)
      return fGetSimulator(simHandle);
    else
      throw std::runtime_error("MaestroLibrary: Unable to get the simulator.");
  }

  void DestroySimulator(unsigned long int simHandle) {
    if (maestro && fDestroySimulator)
      fDestroySimulator(simHandle);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to destroy the simulator.");
  }

  int InitializeSimulator(void* sim) {
    if (maestro && sim && fInitializeSimulator)
      return fInitializeSimulator(sim);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to initialize the simulator.");

    return 0;
  }

  int ResetSimulator(void* sim) {
    if (maestro && sim && fResetSimulator)
      return fResetSimulator(sim);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to reset the simulator.");

    return 0;
  }

  int ConfigureSimulator(void* sim, const char* key, const char* value) {
    if (maestro && sim && fConfigureSimulator)
      return fConfigureSimulator(sim, key, value);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to configure the simulator.");

    return 0;
  }

  char* GetConfiguration(void* sim, const char* key) {
    if (maestro && sim && fGetConfiguration)
      return fGetConfiguration(sim, key);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to get the configuration of the simulator.");
    return nullptr;
  }

  unsigned long int AllocateQubits(void* sim, unsigned long int nrQubits) {
    if (maestro && sim && fAllocateQubits)
      return fAllocateQubits(sim, nrQubits);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to allocate qubits in the simulator.");
    return 0;
  }

  unsigned long int GetNumberOfQubits(void* sim) {
    if (maestro && sim && fGetNumberOfQubits)
      return fGetNumberOfQubits(sim);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to get the number of qubits in the "
          "simulator.");
    return 0;
  }

  int ClearSimulator(void* sim) {
    if (maestro && sim && fClearSimulator)
      return fClearSimulator(sim);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to clear the simulator.");
    return 0;
  }

  unsigned long long int Measure(void* sim, const unsigned long int* qubits,
                                 unsigned long int nrQubits) {
    if (maestro && sim && fMeasure)
      return fMeasure(sim, qubits, nrQubits);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to measure the simulator.");
    return 0;
  }

  int ApplyReset(void* sim, const unsigned long int* qubits,
                 unsigned long int nrQubits) {
    if (maestro && sim && fApplyReset)
      return fApplyReset(sim, qubits, nrQubits);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to apply reset to the simulator.");
    return 0;
  }

  double Probability(void* sim, unsigned long long int outcome) {
    if (maestro && sim && fProbability)
      return fProbability(sim, outcome);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to get the probability of an outcome.");
    return 0.0;
  }

  virtual void FreeDoubleVector(double* vec) {
    if (maestro && fFreeDoubleVector)
      fFreeDoubleVector(vec);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to free the double vector.");
  }

  virtual void FreeULLIVector(unsigned long long int* vec) {
    if (maestro && fFreeULLIVector)
      fFreeULLIVector(vec);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to free the unsigned long long int vector.");
  }

  double* Amplitude(void* sim, unsigned long long int outcome) {
    if (maestro && sim && fAmplitude)
      return fAmplitude(sim, outcome);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to get the amplitude of an outcome.");
    return nullptr;
  }

  double* AllProbabilities(void* sim) {
    if (maestro && sim && fAllProbabilities)
      return fAllProbabilities(sim);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to get all probabilities.");
    return nullptr;
  }

  double* Probabilities(void* sim, const unsigned long long int* qubits,
                        unsigned long int nrQubits) {
    if (maestro && sim && fProbabilities)
      return fProbabilities(sim, qubits, nrQubits);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to get probabilities for specified qubits.");
    return nullptr;
  }

  unsigned long long int* SampleCounts(void* sim,
                                       const unsigned long long int* qubits,
                                       unsigned long int nrQubits,
                                       unsigned long int shots) {
    if (maestro && sim && fSampleCounts)
      return fSampleCounts(sim, qubits, nrQubits, shots);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to get sample counts for specified qubits.");
    return nullptr;
  }

  int GetSimulatorType(void* sim) {
    if (maestro && sim && fGetSimulatorType)
      return fGetSimulatorType(sim);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to get the simulator type.");
    return -1;
  }

  int GetSimulationType(void* sim) {
    if (maestro && sim && fGetSimulationType)
      return fGetSimulationType(sim);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to get the simulation type.");
    return -1;
  }

  int FlushSimulator(void* sim) {
    if (maestro && sim && fFlushSimulator)
      return fFlushSimulator(sim);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to flush the simulator.");
    return 0;
  }

  int SaveStateToInternalDestructive(void* sim) {
    if (maestro && sim && fSaveStateToInternalDestructive)
      return fSaveStateToInternalDestructive(sim);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to save the state to internal destructive "
          "storage.");
    return 0;
  }

  int RestoreInternalDestructiveSavedState(void* sim) {
    if (maestro && sim && fRestoreInternalDestructiveSavedState)
      return fRestoreInternalDestructiveSavedState(sim);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to restore the state from internal "
          "destructive storage.");
    return 0;
  }

  int SaveState(void* sim) {
    if (maestro && sim && fSaveState)
      return fSaveState(sim);
    else
      throw std::runtime_error("MaestroLibrary: Unable to save the state.");
    return 0;
  }

  int RestoreState(void* sim) {
    if (maestro && sim && fRestoreState)
      return fRestoreState(sim);
    else
      throw std::runtime_error("MaestroLibrary: Unable to restore the state.");
    return 0;
  }

  int SetMultithreading(void* sim, int multithreading) {
    if (maestro && sim && fSetMultithreading)
      return fSetMultithreading(sim, multithreading);
    else
      throw std::runtime_error("MaestroLibrary: Unable to set multithreading.");
    return 0;
  }

  int GetMultithreading(void* sim) {
    if (maestro && sim && fGetMultithreading)
      return fGetMultithreading(sim);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to get multithreading status.");
    return 0;
  }

  int IsQcsim(void* sim) {
    if (maestro && sim && fIsQcsim)
      return fIsQcsim(sim);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to check if the simulator is a QCSIM.");
    return 0;
  }

  unsigned long long int MeasureNoCollapse(void* sim) {
    if (maestro && sim && fMeasureNoCollapse)
      return fMeasureNoCollapse(sim);
    else
      throw std::runtime_error(
          "MaestroLibrary: Unable to measure without collapse.");
    return 0;
  }

  int ApplyX(void* sim, int qubit) {
    if (maestro && sim && fApplyX)
      return fApplyX(sim, qubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply X gate.");
    return 0;
  }

  int ApplyY(void* sim, int qubit) {
    if (maestro && sim && fApplyY)
      return fApplyY(sim, qubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply Y gate.");
    return 0;
  }

  int ApplyZ(void* sim, int qubit) {
    if (maestro && sim && fApplyZ)
      return fApplyZ(sim, qubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply Z gate.");
    return 0;
  }

  int ApplyH(void* sim, int qubit) {
    if (maestro && sim && fApplyH)
      return fApplyH(sim, qubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply H gate.");
    return 0;
  }

  int ApplyS(void* sim, int qubit) {
    if (maestro && sim && fApplyS)
      return fApplyS(sim, qubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply S gate.");
    return 0;
  }

  int ApplySDG(void* sim, int qubit) {
    if (maestro && sim && fApplySDG)
      return fApplySDG(sim, qubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply SDG gate.");
    return 0;
  }

  int ApplyT(void* sim, int qubit) {
    if (maestro && sim && fApplyT)
      return fApplyT(sim, qubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply T gate.");
    return 0;
  }

  int ApplyTDG(void* sim, int qubit) {
    if (maestro && sim && fApplyTDG)
      return fApplyTDG(sim, qubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply TDG gate.");
    return 0;
  }

  int ApplySX(void* sim, int qubit) {
    if (maestro && sim && fApplySX)
      return fApplySX(sim, qubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply SX gate.");
    return 0;
  }

  int ApplySXDG(void* sim, int qubit) {
    if (maestro && sim && fApplySXDG)
      return fApplySXDG(sim, qubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply SXDG gate.");
    return 0;
  }

  int ApplyK(void* sim, int qubit) {
    if (maestro && sim && fApplyK)
      return fApplyK(sim, qubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply K gate.");
    return 0;
  }

  int ApplyP(void* sim, int qubit, double theta) {
    if (maestro && sim && fApplyP)
      return fApplyP(sim, qubit, theta);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply P gate.");
    return 0;
  }

  int ApplyRx(void* sim, int qubit, double theta) {
    if (maestro && sim && fApplyRx)
      return fApplyRx(sim, qubit, theta);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply Rx gate.");
    return 0;
  }

  int ApplyRy(void* sim, int qubit, double theta) {
    if (maestro && sim && fApplyRy)
      return fApplyRy(sim, qubit, theta);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply Ry gate.");
    return 0;
  }

  int ApplyRz(void* sim, int qubit, double theta) {
    if (maestro && sim && fApplyRz)
      return fApplyRz(sim, qubit, theta);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply Rz gate.");
    return 0;
  }

  int ApplyU(void* sim, int qubit, double theta, double phi, double lambda,
             double gamma) {
    if (maestro && sim && fApplyU)
      return fApplyU(sim, qubit, theta, phi, lambda, gamma);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply U gate.");
    return 0;
  }

  int ApplyCX(void* sim, int controlQubit, int targetQubit) {
    if (maestro && sim && fApplyCX)
      return fApplyCX(sim, controlQubit, targetQubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply CX gate.");
    return 0;
  }

  int ApplyCY(void* sim, int controlQubit, int targetQubit) {
    if (maestro && sim && fApplyCY)
      return fApplyCY(sim, controlQubit, targetQubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply CY gate.");
    return 0;
  }

  int ApplyCZ(void* sim, int controlQubit, int targetQubit) {
    if (maestro && sim && fApplyCZ)
      return fApplyCZ(sim, controlQubit, targetQubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply CZ gate.");
    return 0;
  }

  int ApplyCH(void* sim, int controlQubit, int targetQubit) {
    if (maestro && sim && fApplyCH)
      return fApplyCH(sim, controlQubit, targetQubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply CH gate.");
    return 0;
  }

  int ApplyCSX(void* sim, int controlQubit, int targetQubit) {
    if (maestro && sim && fApplyCSX)
      return fApplyCSX(sim, controlQubit, targetQubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply CSX gate.");
    return 0;
  }

  int ApplyCSXDG(void* sim, int controlQubit, int targetQubit) {
    if (maestro && sim && fApplyCSXDG)
      return fApplyCSXDG(sim, controlQubit, targetQubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply CSXDG gate.");
    return 0;
  }

  int ApplyCP(void* sim, int controlQubit, int targetQubit, double theta) {
    if (maestro && sim && fApplyCP)
      return fApplyCP(sim, controlQubit, targetQubit, theta);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply CP gate.");
    return 0;
  }

  int ApplyCRx(void* sim, int controlQubit, int targetQubit, double theta) {
    if (maestro && sim && fApplyCRx)
      return fApplyCRx(sim, controlQubit, targetQubit, theta);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply CRx gate.");
    return 0;
  }

  int ApplyCRy(void* sim, int controlQubit, int targetQubit, double theta) {
    if (maestro && sim && fApplyCRy)
      return fApplyCRy(sim, controlQubit, targetQubit, theta);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply CRy gate.");
    return 0;
  }

  int ApplyCRz(void* sim, int controlQubit, int targetQubit, double theta) {
    if (maestro && sim && fApplyCRz)
      return fApplyCRz(sim, controlQubit, targetQubit, theta);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply CRz gate.");
    return 0;
  }

  int ApplyCCX(void* sim, int controlQubit1, int controlQubit2,
               int targetQubit) {
    if (maestro && sim && fApplyCCX)
      return fApplyCCX(sim, controlQubit1, controlQubit2, targetQubit);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply CCX gate.");
    return 0;
  }

  int ApplySwap(void* sim, int qubit1, int qubit2) {
    if (maestro && sim && fApplySwap)
      return fApplySwap(sim, qubit1, qubit2);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply Swap gate.");
    return 0;
  }

  int ApplyCSwap(void* sim, int controlQubit, int qubit1, int qubit2) {
    if (maestro && sim && fApplyCSwap)
      return fApplyCSwap(sim, controlQubit, qubit1, qubit2);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply CSwap gate.");
    return 0;
  }

  int ApplyCU(void* sim, int controlQubit, int targetQubit, double theta,
              double phi, double lambda, double gamma) {
    if (maestro && sim && fApplyCU)
      return fApplyCU(sim, controlQubit, targetQubit, theta, phi, lambda,
                      gamma);
    else
      throw std::runtime_error("MaestroLibrary: Unable to apply CU gate.");
    return 0;
  }

 private:
  void* maestro = nullptr;

  void* (*fGetMaestroObject)();

  unsigned long int (*fCreateSimpleSimulator)(int);
  void (*fDestroySimpleSimulator)(unsigned long int);

  int (*fRemoveAllOptimizationSimulatorsAndAdd)(unsigned long int, int, int);
  int (*fAddOptimizationSimulator)(unsigned long int, int, int);

  char* (*fSimpleExecute)(unsigned long int, const char*, const char*);
  char* (*fSimpleEstimate)(unsigned long int, const char*, const char*,
                           const char*);
  void (*fFreeResult)(char*);

  unsigned long int (*fCreateSimulator)(int, int);
  void* (*fGetSimulator)(unsigned long int);
  void (*fDestroySimulator)(unsigned long int);

  int (*fInitializeSimulator)(void*);
  int (*fResetSimulator)(void*);
  int (*fConfigureSimulator)(void*, const char*, const char*);
  char* (*fGetConfiguration)(void*, const char*);
  unsigned long int (*fAllocateQubits)(void*, unsigned long int);
  unsigned long int (*fGetNumberOfQubits)(void*);
  int (*fClearSimulator)(void*);
  unsigned long long int (*fMeasure)(void*, const unsigned long int*,
                                     unsigned long int);
  int (*fApplyReset)(void*, const unsigned long int*, unsigned long int);
  double (*fProbability)(void*, unsigned long long int);
  void (*fFreeDoubleVector)(double*);
  void (*fFreeULLIVector)(unsigned long long int*);
  double* (*fAmplitude)(void*, unsigned long long int);
  double* (*fAllProbabilities)(void*);
  double* (*fProbabilities)(void*, const unsigned long long int*,
                            unsigned long int);
  unsigned long long int* (*fSampleCounts)(void*, const unsigned long long int*,
                                           unsigned long int,
                                           unsigned long int);
  int (*fGetSimulatorType)(void*);
  int (*fGetSimulationType)(void*);
  int (*fFlushSimulator)(void*);
  int (*fSaveStateToInternalDestructive)(void*);
  int (*fRestoreInternalDestructiveSavedState)(void*);
  int (*fSaveState)(void*);
  int (*fRestoreState)(void*);
  int (*fSetMultithreading)(void*, int);
  int (*fGetMultithreading)(void*);
  int (*fIsQcsim)(void*);
  unsigned long long int (*fMeasureNoCollapse)(void*);

  int (*fApplyX)(void*, int);
  int (*fApplyY)(void*, int);
  int (*fApplyZ)(void*, int);
  int (*fApplyH)(void*, int);
  int (*fApplyS)(void*, int);
  int (*fApplySDG)(void*, int);
  int (*fApplyT)(void*, int);
  int (*fApplyTDG)(void*, int);
  int (*fApplySX)(void*, int);
  int (*fApplySXDG)(void*, int);
  int (*fApplyK)(void*, int);
  int (*fApplyP)(void*, int, double);
  int (*fApplyRx)(void*, int, double);
  int (*fApplyRy)(void*, int, double);
  int (*fApplyRz)(void*, int, double);
  int (*fApplyU)(void*, int, double, double, double, double);
  int (*fApplyCX)(void*, int, int);
  int (*fApplyCY)(void*, int, int);
  int (*fApplyCZ)(void*, int, int);
  int (*fApplyCH)(void*, int, int);
  int (*fApplyCSX)(void*, int, int);
  int (*fApplyCSXDG)(void*, int, int);
  int (*fApplyCP)(void*, int, int, double);
  int (*fApplyCRx)(void*, int, int, double);
  int (*fApplyCRy)(void*, int, int, double);
  int (*fApplyCRz)(void*, int, int, double);
  int (*fApplyCCX)(void*, int, int, int);
  int (*fApplySwap)(void*, int, int);
  int (*fApplyCSwap)(void*, int, int, int);
  int (*fApplyCU)(void*, int, int, double, double, double, double);
};
