#ifndef LINEAR_SOLVER_OPTIONS_HPP
#define LINEAR_SOLVER_OPTIONS_HPP

#include <utils/Exceptions.hpp>

#include <iostream>

enum class LSLibrary : int8_t
{
  LS__begin = 0,
  //
  builtin = LS__begin,
  petsc,
  //
  LS__end
};

enum class LSMethod : int8_t
{
  LS__begin = 0,
  //
  cg = LS__begin,
  bicgstab,
  bicgstab2,
  gmres,
  lu,
  choleski,
  //
  LS__end
};

enum class LSPrecond : int8_t
{
  LS__begin = 0,
  //
  none = LS__begin,
  diagonal,
  incomplete_choleski,
  incomplete_LU,
  amg,
  //
  LS__end
};

inline std::string
name(const LSLibrary library)
{
  switch (library) {
  case LSLibrary::builtin: {
    return "builtin";
  }
  case LSLibrary::petsc: {
    return "PETSc";
  }
  case LSLibrary::LS__end: {
  }
  }
  throw UnexpectedError("Linear system library name is not defined!");
}

inline std::string
name(const LSMethod method)
{
  switch (method) {
  case LSMethod::cg: {
    return "CG";
  }
  case LSMethod::bicgstab: {
    return "BICGStab";
  }
  case LSMethod::bicgstab2: {
    return "BICGStab2";
  }
  case LSMethod::gmres: {
    return "GMRES";
  }
  case LSMethod::lu: {
    return "LU";
  }
  case LSMethod::choleski: {
    return "Choleski";
  }
  case LSMethod::LS__end: {
  }
  }
  throw UnexpectedError("Linear system method name is not defined!");
}

inline std::string
name(const LSPrecond precond)
{
  switch (precond) {
  case LSPrecond::none: {
    return "none";
  }
  case LSPrecond::diagonal: {
    return "diagonal";
  }
  case LSPrecond::incomplete_choleski: {
    return "ICholeski";
  }
  case LSPrecond::incomplete_LU: {
    return "ILU";
  }
  case LSPrecond::amg: {
    return "AMG";
  }
  case LSPrecond::LS__end: {
  }
  }
  throw UnexpectedError("Linear system preconditioner name is not defined!");
}

template <typename LSEnumType>
inline LSEnumType
getLSEnumFromName(const std::string& enum_name)
{
  using BaseT = std::underlying_type_t<LSEnumType>;
  for (BaseT enum_value = static_cast<BaseT>(LSEnumType::LS__begin);
       enum_value < static_cast<BaseT>(LSEnumType::LS__end); ++enum_value) {
    if (name(LSEnumType{enum_value}) == enum_name) {
      return LSEnumType{enum_value};
    }
  }
  throw NormalError(std::string{"could not find '"} + enum_name + "' associate type!");
}

template <typename LSEnumType>
inline void
printLSEnumListNames(std::ostream& os)
{
  using BaseT = std::underlying_type_t<LSEnumType>;
  for (BaseT enum_value = static_cast<BaseT>(LSEnumType::LS__begin);
       enum_value < static_cast<BaseT>(LSEnumType::LS__end); ++enum_value) {
    os << "  - " << name(LSEnumType{enum_value}) << '\n';
  }
}

class LinearSolverOptions
{
 private:
  LSLibrary m_library = LSLibrary::builtin;
  LSMethod m_method   = LSMethod::bicgstab;
  LSPrecond m_precond = LSPrecond::none;

  double m_epsilon           = 1E-6;
  size_t m_maximum_iteration = 200;

  bool m_verbose = false;

 public:
  static LinearSolverOptions default_options;

  friend std::ostream& operator<<(std::ostream& os, const LinearSolverOptions& options);

  LSLibrary&
  library()
  {
    return m_library;
  }

  LSLibrary
  library() const
  {
    return m_library;
  }

  LSMethod
  method() const
  {
    return m_method;
  }

  LSMethod&
  method()
  {
    return m_method;
  }

  LSPrecond
  precond() const
  {
    return m_precond;
  }

  LSPrecond&
  precond()
  {
    return m_precond;
  }

  double
  epsilon() const
  {
    return m_epsilon;
  }

  double&
  epsilon()
  {
    return m_epsilon;
  }

  size_t&
  maximumIteration()
  {
    return m_maximum_iteration;
  }

  size_t
  maximumIteration() const
  {
    return m_maximum_iteration;
  }

  bool&
  verbose()
  {
    return m_verbose;
  };

  bool
  verbose() const
  {
    return m_verbose;
  };

  LinearSolverOptions(const LinearSolverOptions&) = default;
  LinearSolverOptions(LinearSolverOptions&&)      = default;

  LinearSolverOptions()  = default;
  ~LinearSolverOptions() = default;
};

inline LinearSolverOptions LinearSolverOptions::default_options;

#endif   // LINEAR_SOLVER_OPTIONS_HPP