/**
 * @file Factory.cpp
 * @version 1.0
 *
 * @section DESCRIPTION
 *
 * Simulator factory implementation
 *
 * Call CreateSimulator with the desired simulator type to create a simulator
 * returned as a shared pointer. Currently only two simulators are supported:
 * qiskit aer and qcsim. Can be esily extended to support more simulators. Just
 * implement the interface for another simulator, add its type to the enum and
 * add another case to the switch statement.
 */

#define _CRT_SECURE_NO_WARNINGS 1

#ifndef NO_QISKIT_AER
#ifndef __APPLE__
#ifndef _QV_AVX2_IMPL
#define _QV_AVX2_IMPL
#pragma warning(push)
#pragma warning(disable : 4789)
#include "simulators/statevector/qv_avx2.cpp"
#pragma warning(pop)
#endif
#endif
#endif

#include "Factory.h"

#define INCLUDED_BY_FACTORY
#ifndef NO_QISKIT_AER
#include "AerSimulator.h"
#endif
#include "QCSimSimulator.h"
#include "Composite.h"
#include "GpuSimulator.h"
#include "QuestSimulator.h"

namespace Simulators {

#ifdef __linux__
std::shared_ptr<GpuLibrary> SimulatorsFactory::gpuLibrary = nullptr;
std::atomic_bool SimulatorsFactory::firstTime = true;

bool SimulatorsFactory::InitGpuLibrary() {
  if (!gpuLibrary) {
    gpuLibrary = std::make_shared<GpuLibrary>();
    if (!firstTime.exchange(false)) gpuLibrary->SetMute(true);

    if (gpuLibrary->Init("libcomposer_gpu_simulators.so"))
      return true;
    else
      gpuLibrary = nullptr;
  }

  return false;
}

bool SimulatorsFactory::InitGpuLibraryWithMute() {
  if (!gpuLibrary) {
    gpuLibrary = std::make_shared<GpuLibrary>();
    firstTime = false;
    gpuLibrary->SetMute(true);

    if (gpuLibrary->Init("libcomposer_gpu_simulators.so"))
      return true;
    else
      gpuLibrary = nullptr;
  }

  return false;
}

#endif

std::shared_ptr<QuestLibSim> SimulatorsFactory::questLibrary = nullptr;
std::atomic_bool SimulatorsFactory::firstTimeQuest = true;

bool SimulatorsFactory::InitQuestLibrary() {
  if (!questLibrary) {
    questLibrary = std::make_shared<QuestLibSim>();
    if (!firstTimeQuest.exchange(false)) questLibrary->SetMute(true);
    if (questLibrary->Init(
#ifdef _WIN32
            "maestroquest.dll"
#elif defined(__APPLE__)
            "libmaestroquest.dylib"
#else
            "libmaestroquest.so"
#endif
            ))
      return true;
    else
      questLibrary = nullptr;
  }
  return false;
}

std::shared_ptr<ISimulator> SimulatorsFactory::CreateSimulator(
    SimulatorType t, SimulationType m) {
  switch (t) {
    case SimulatorType::kQCSim: {
      auto sim = std::make_shared<Private::QCSimSimulator>();
      if (m == SimulationType::kMatrixProductState)
        sim->Configure("method", "matrix_product_state");
      else if (m == SimulationType::kStabilizer)
        sim->Configure("method", "stabilizer");
      else if (m == SimulationType::kTensorNetwork)
        sim->Configure("method", "tensor_network");
      else if (m == SimulationType::kPauliPropagator)
        sim->Configure("method", "pauli_propagator");
      else if (m != SimulationType::kStatevector)
        throw std::invalid_argument("Simulation Type not supported for QCSim");

      return sim;
    }
#ifndef NO_QISKIT_AER
    case SimulatorType::kQiskitAer: {
      auto sim = std::make_shared<Private::AerSimulator>();
      if (m == SimulationType::kMatrixProductState)
        sim->Configure("method", "matrix_product_state");
      else if (m == SimulationType::kStabilizer)
        sim->Configure("method", "stabilizer");
      else if (m == SimulationType::kTensorNetwork)
        sim->Configure("method", "tensor_network");
      else if (m == SimulationType::kExtendedStabilizer)
        sim->Configure("method", "extended_stabilizer");
      else if (m == SimulationType::kStatevector)
        sim->Configure("method", "statevector");
      else
        throw std::invalid_argument(
            "Simulation Type not supported for Qiskit Aer");

      return sim;
    }
    case SimulatorType::kCompositeQiskitAer:
      return std::make_shared<Private::CompositeSimulator>(
          SimulatorType::kQiskitAer);
#endif
    case SimulatorType::kCompositeQCSim:
      return std::make_shared<Private::CompositeSimulator>(
          SimulatorType::kQCSim);
#ifdef __linux__
    case SimulatorType::kGpuSim:
      if (gpuLibrary && gpuLibrary->IsValid() &&
          (m == SimulationType::kStatevector ||
           m == SimulationType::kMatrixProductState ||
           m == SimulationType::kTensorNetwork ||
           m == SimulationType::kPauliPropagator)) {
        auto sim = std::make_shared<Private::GpuSimulator>();
        if (m == SimulationType::kMatrixProductState)
          sim->Configure("method", "matrix_product_state");
        else if (m == SimulationType::kTensorNetwork)
          sim->Configure("method", "tensor_network");
        else if (m == SimulationType::kPauliPropagator)
          sim->Configure("method", "pauli_propagator");
        else if (m == SimulationType::kStatevector)
          sim->Configure("method", "statevector");

        return sim;
      }

      return nullptr;
#endif
    case SimulatorType::kQuestSim:
      if (m != SimulationType::kStatevector)
        throw std::invalid_argument(
            "Simulation Type not supported for Quest Simulator");
      else if (questLibrary && questLibrary->IsValid()) {
        return std::make_shared<Private::QuestSimulator>();
      }
      return nullptr;
    default:
      break;
  }

  throw std::invalid_argument("Simulator Type not supported");

  return nullptr;  // keep compillers happy
}

std::unique_ptr<ISimulator> SimulatorsFactory::CreateSimulatorUnique(
    SimulatorType t, SimulationType m) {
  switch (t) {
    case SimulatorType::kQCSim: {
      auto sim = std::make_unique<Private::QCSimSimulator>();
      if (m == SimulationType::kMatrixProductState)
        sim->Configure("method", "matrix_product_state");
      else if (m == SimulationType::kStabilizer)
        sim->Configure("method", "stabilizer");
      else if (m == SimulationType::kTensorNetwork)
        sim->Configure("method", "tensor_network");
      else if (m == SimulationType::kPauliPropagator)
        sim->Configure("method", "pauli_propagator");
      else if (m != SimulationType::kStatevector)
        throw std::invalid_argument("Simulation Type not supported for QCSim");

      return sim;
    }
#ifndef NO_QISKIT_AER
    case SimulatorType::kQiskitAer: {
      auto sim = std::make_unique<Private::AerSimulator>();
      if (m == SimulationType::kMatrixProductState)
        sim->Configure("method", "matrix_product_state");
      else if (m == SimulationType::kStabilizer)
        sim->Configure("method", "stabilizer");
      else if (m == SimulationType::kTensorNetwork)
        sim->Configure("method", "tensor_network");
      else if (m == SimulationType::kExtendedStabilizer)
        sim->Configure("method", "extended_stabilizer");
      else if (m == SimulationType::kStatevector)
        sim->Configure("method", "statevector");
      else
        throw std::invalid_argument(
            "Simulation Type not supported for Qiskit Aer");

      return sim;
    }
    case SimulatorType::kCompositeQiskitAer:
      return std::make_unique<Private::CompositeSimulator>(
          SimulatorType::kQiskitAer);
#endif
    case SimulatorType::kCompositeQCSim:
      return std::make_unique<Private::CompositeSimulator>(
          SimulatorType::kQCSim);
#ifdef __linux__
    case SimulatorType::kGpuSim:
      if (gpuLibrary && gpuLibrary->IsValid() &&
          (m == SimulationType::kStatevector ||
           m == SimulationType::kMatrixProductState ||
           m == SimulationType::kTensorNetwork ||
           m == SimulationType::kPauliPropagator)) {
        auto sim = std::make_unique<Private::GpuSimulator>();
        if (m == SimulationType::kMatrixProductState)
          sim->Configure("method", "matrix_product_state");
        else if (m == SimulationType::kTensorNetwork)
          sim->Configure("method", "tensor_network");
        else if (m == SimulationType::kPauliPropagator)
          sim->Configure("method", "pauli_propagator");
        else if (m == SimulationType::kStatevector)
          sim->Configure("method", "statevector");

        return sim;
      }

      return nullptr;
#endif
    case SimulatorType::kQuestSim:
      if (m != SimulationType::kStatevector)
        throw std::invalid_argument(
            "Simulation Type not supported for Quest Simulator");
      else if (questLibrary && questLibrary->IsValid()) {
        return std::make_unique<Private::QuestSimulator>();
      }
      return nullptr;
    default:
      break;
  }

  throw std::invalid_argument("Simulator Type not supported");

  return nullptr;  // keep compillers happy
}
}  // namespace Simulators
