#ifndef PUGS_ASSERT_HPP
#define PUGS_ASSERT_HPP

#include <utils/PugsMacros.hpp>
#include <utils/PugsTraits.hpp>

#include <iostream>
#include <rang.hpp>
#include <string>
#include <tuple>

class AssertError : public std::runtime_error
{
 private:
  const std::string m_file;
  const size_t m_line;
  const std::string m_function;
  const std::string m_test;
  const std::string m_message;

 public:
  friend std::ostream&
  operator<<(std::ostream& os, const AssertError& assert_error)
  {
    os << '\n'
       << "---------- Assertion error -----------\n"
       << " at " << assert_error.m_file << ':' << assert_error.m_line << '\n'
       << " in " << assert_error.m_function << '\n'
       << " assertion (" << assert_error.m_test << ") failed!\n";
    if (not assert_error.m_message.empty()) {
      os << ' ' << assert_error.m_message << '\n';
    }
    os << "--------------------------------------" << '\n';

    return os;
  }

  template <typename... Args>
  AssertError(const std::string& filename,
              size_t line,
              const std::string& function,
              const std::tuple<Args...>& tuple_args,
              const std::string_view args_string)
    : std::runtime_error(""),
      m_file{filename},
      m_line{line},
      m_function{function},
      m_test{[&] {
        if constexpr (sizeof...(Args) == 1) {
          return std::string(args_string);
        } else {
          std::string_view message = std::get<1>(tuple_args);
          std::string test_str{args_string, 0, args_string.size() - message.size() - 2};
          while (test_str[test_str.size() - 1] == ' ' or test_str[test_str.size() - 1] == ',') {
            test_str.resize(test_str.size() - 1);
          }
          return test_str;
        }
      }()},
      m_message{[&] {
        if constexpr (sizeof...(Args) == 1) {
          return std::string{};
        } else {
          return std::get<1>(tuple_args);
        }
      }()}
  {
    if (m_message.empty()) {
      std::runtime_error::operator=(std::runtime_error(m_test.c_str()));
    } else {
      std::runtime_error::operator=(std::runtime_error(m_message.c_str()));
    }
  }
};

template <typename... Args>
struct AssertChecker;

template <typename... Args>
struct AssertChecker<std::tuple<Args...> >
{
  void static check_args_type()
  {
    constexpr size_t size = sizeof...(Args);
    static_assert(size >= 1 and size <= 2, "Assert requires 1 or 2 parameters");

    using ArgsTuple = std::tuple<Args...>;

    using assertion_t = std::tuple_element_t<0, ArgsTuple>;
    static_assert(std::is_integral_v<assertion_t> or std::is_pointer_v<assertion_t> or is_std_ptr_v<assertion_t>);

    if constexpr (size == 2) {
      using message_t = std::decay_t<std::remove_const_t<std::tuple_element_t<1, ArgsTuple> > >;

      static_assert(std::is_same_v<message_t, const char*>, "optional second argument must be a string literal");
    }
  }
};

#ifdef NDEBUG

#define Assert(...)                                           \
  {                                                           \
    using TupleArgs = decltype(std::make_tuple(__VA_ARGS__)); \
    AssertChecker<TupleArgs>::check_args_type();              \
  }

#else   // NDEBUG

#define Assert(...)                                                                                                 \
  {                                                                                                                 \
    using TupleArgs = decltype(std::make_tuple(__VA_ARGS__));                                                       \
    AssertChecker<TupleArgs>::check_args_type();                                                                    \
    constexpr int tuple_size = std::tuple_size_v<TupleArgs>;                                                        \
    static_assert(tuple_size >= 1 and tuple_size <= 2);                                                             \
    if (not static_cast<bool>(std::get<0>(std::forward_as_tuple(__VA_ARGS__)))) {                                   \
      throw AssertError(__FILE__, __LINE__, __PRETTY_FUNCTION__, std::forward_as_tuple(__VA_ARGS__), #__VA_ARGS__); \
    }                                                                                                               \
  }

#endif   // NDEBUG

// store the current state of Assert macro. This is useful for
// instance to noexcept management of Assert throws
#ifdef NDEBUG
#define NO_ASSERT true
#else   // NDEBUG
#define NO_ASSERT false
#endif   // NDEBUG

#endif   // PUGS_ASSERT_HPP