/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2024, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */

#pragma once

#include <mrpt/core/abs_diff.h>  // mrpt::abs_diff()
#include <mrpt/core/backtrace.h>
#include <mrpt/core/common.h>
#include <mrpt/core/format.h>

#include <cstdlib>    // std::abs
#include <stdexcept>  // logic_error
#include <string>     // std::string, to_string()
#include <string_view>

namespace mrpt::internal
{
int MAX_BACKTRACE_DEPTH();

std::string exception_line_msg(
    const std::string_view& msg,
    const char* filename,
    unsigned int line,
    const char* function_name);

template <typename A, typename B>
inline std::string asrt_fail(std::string s, A&& a, B&& b, const char* astr, const char* bstr)
{
  s += "(";
  s += astr;
  s += ",";
  s += bstr;
  s += ") failed with\n";
  s += astr;
  s += "=";
  s += mrpt::to_string(a);
  s += "\n";
  s += bstr;
  s += "=";
  s += mrpt::to_string(b);
  s += "\n";
  return s;
}
}  // namespace mrpt::internal

namespace mrpt
{
/** Builds a nice textual representation of an exception, which if
 * generated using MRPT macros (THROW_EXCEPTION,...) in between
 * MRPT_START/MRPT_END macros, will contain function names and line numbers
 * across the call stack at the original throw point.
 * Since MRPT 2.1.5 this will also print line numbers of any other exception
 * (even if generated in non-mrpt code, like inside STL headers)
 * as long as MRPT was built with BFD support and sources built with, at least,
 * -g1 debug symbols.
 *
 * See examples of use in \ref mrpt_core_grp
 * \ingroup mrpt_core_grp
 */
std::string exception_to_str(const std::exception& e);

struct ExceptionWithCallBackBase
{
  ExceptionWithCallBackBase(const std::string original_what, const TCallStackBackTrace call_stack) :
      originalWhat(original_what), callStack(call_stack)
  {
  }

  const std::string originalWhat;
  const TCallStackBackTrace callStack;
};

/** A wrapper around an std::exception */
template <class BASE_EXCEPTION>
struct ExceptionWithCallBack : public BASE_EXCEPTION, public ExceptionWithCallBackBase
{
  ExceptionWithCallBack(const BASE_EXCEPTION& originalException) :
      BASE_EXCEPTION(originalException),
      ExceptionWithCallBackBase(
          originalException.what(),
          mrpt::callStackBackTrace(2 /*skip 2 frames*/, 2 + mrpt::internal::MAX_BACKTRACE_DEPTH()))
  {
  }

  /** Use this pointer only before this object is destroyed */
  const char* what() const noexcept override
  {
    if (m_what.empty()) m_what = mrpt::exception_to_str(*this);
    return m_what.c_str();
  }

 private:
  mutable std::string m_what;
};

/** \def THROW_TYPED_EXCEPTION(msg,exceptionClass) */
#define THROW_TYPED_EXCEPTION(msg, exceptionClass)                  \
  throw mrpt::ExceptionWithCallBack<exceptionClass>(exceptionClass( \
      mrpt::internal::exception_line_msg(msg, __FILE__, __LINE__, __CURRENT_FUNCTION_NAME__)))

/** \def THROW_EXCEPTION(msg);
 * \param msg This can be a char*, a std::string, or a literal string.
 * Defines a unified way of reporting exceptions
 * \sa MRPT_TRY_START, MRPT_TRY_END, THROW_EXCEPTION_FMT
 */
#define THROW_EXCEPTION(msg) THROW_TYPED_EXCEPTION(msg, std::logic_error)

#define THROW_EXCEPTION_FMT(_FORMAT_STRING, ...) \
  THROW_EXCEPTION(mrpt::format(_FORMAT_STRING, __VA_ARGS__))

#define THROW_TYPED_EXCEPTION_FMT(exceptionClass, _FORMAT_STRING, ...) \
  THROW_TYPED_EXCEPTION(mrpt::format(_FORMAT_STRING, __VA_ARGS__), exceptionClass)

/** For use in CSerializable implementations */
#define MRPT_THROW_UNKNOWN_SERIALIZATION_VERSION(__V) \
  THROW_EXCEPTION_FMT(                                \
      "Cannot parse object: unknown serialization version number: '%i'", static_cast<int>(__V))

/** Throws a stacked exception if condition "f" is false; with custom message.
 * \sa MRPT_TRY_START, MRPT_TRY_END
 */
#define ASSERTMSG_(f, __ERROR_MSG)                         \
  do                                                       \
  {                                                        \
    if (!(f)) THROW_EXCEPTION(::std::string(__ERROR_MSG)); \
  } while (0)

/** Throws a stacked exception if condition "f" is false.
 * \sa MRPT_TRY_START, MRPT_TRY_END
 */
#define ASSERT_(f) ASSERTMSG_(f, std::string("Assert condition failed: ") + ::std::string(#f))

/** Throws an exception if the number is NaN, IND, or +/-INF, or return the same
 * number otherwise. */
#define MRPT_CHECK_NORMAL_NUMBER(v) \
  do                                \
  {                                 \
    ASSERT_(std::isfinite(v));      \
    ASSERT_(!std::isnan(v));        \
  } while (0)

#define ASRT_FAIL(__CONDITIONSTR, __A, __B, __ASTR, __BSTR) \
  THROW_EXCEPTION(mrpt::internal::asrt_fail(__CONDITIONSTR, __A, __B, __ASTR, __BSTR));

/** Assert comparing two values, reporting their actual values upon failure */
#define ASSERT_EQUAL_(__A, __B)                                      \
  do                                                                 \
  {                                                                  \
    if (__A != __B) ASRT_FAIL("ASSERT_EQUAL_", __A, __B, #__A, #__B) \
  } while (0)

#define ASSERT_NOT_EQUAL_(__A, __B)                                      \
  do                                                                     \
  {                                                                      \
    if (__A == __B) ASRT_FAIL("ASSERT_NOT_EQUAL_", __A, __B, #__A, #__B) \
  } while (0)

/** Checks two float/double values, reporting their values upon failure.
 * \note If A, B, and Tolerance are of different types, keep in mind that they
 *       will be compared after converting into typeof(A).
 */
#define ASSERT_NEAR_(__A, __B, __TOLERANCE)                                      \
  do                                                                             \
  {                                                                              \
    const auto diff = mrpt::abs_diff<std::decay_t<decltype(__A)>>((__A), (__B)); \
    if (diff > __TOLERANCE) ASRT_FAIL("ASSERT_NEAR_", __A, __B, #__A, #__B)      \
  } while (0)

/** Checks A<B */
#define ASSERT_LT_(__A, __B)                                      \
  do                                                              \
  {                                                               \
    if (__A >= __B) ASRT_FAIL("ASSERT_LT_", __A, __B, #__A, #__B) \
  } while (0)

/** Checks A<=B */
#define ASSERT_LE_(__A, __B)                                     \
  do                                                             \
  {                                                              \
    if (__A > __B) ASRT_FAIL("ASSERT_LE_", __A, __B, #__A, #__B) \
  } while (0)

/** Checks A>B */
#define ASSERT_GT_(__A, __B)                                      \
  do                                                              \
  {                                                               \
    if (__A <= __B) ASRT_FAIL("ASSERT_GT_", __A, __B, #__A, #__B) \
  } while (0)

/** Checks A>=B */
#define ASSERT_GE_(__A, __B)                                     \
  do                                                             \
  {                                                              \
    if (__A < __B) ASRT_FAIL("ASSERT_GE_", __A, __B, #__A, #__B) \
  } while (0)

#ifdef _DEBUG
#define ASSERTDEB_(f)                  ASSERT_(f)
#define ASSERTDEBMSG_(f, __ERROR_MSG)  ASSERTMSG_(f, __ERROR_MSG)
#define ASSERTDEB_EQUAL_(__A, __B)     ASSERT_EQUAL_(__A, __B)
#define ASSERTDEB_NOT_EQUAL_(__A, __B) ASSERT_NOT_EQUAL_(__A, __B)
#define ASSERTDEB_LT_(__A, __B)        ASSERT_LT_(__A, __B)
#define ASSERTDEB_GT_(__A, __B)        ASSERT_GT_(__A, __B)
#define ASSERTDEB_LE_(__A, __B)        ASSERT_LE_(__A, __B)
#define ASSERTDEB_GE_(__A, __B)        ASSERT_GE_(__A, __B)
#else
// clang-format off
#define ASSERTDEB_(f) while (0){}
#define ASSERTDEBMSG_(f, __ERROR_MSG) while (0){}
#define ASSERTDEB_EQUAL_(__A, __B) while (0){}
#define ASSERTDEB_NOT_EQUAL_(__A, __B) while (0){}
#define ASSERTDEB_LT_(__A, __B) while (0){}
#define ASSERTDEB_GT_(__A, __B) while (0){}
#define ASSERTDEB_LE_(__A, __B) while (0){}
#define ASSERTDEB_GE_(__A, __B) while (0){}
// clang-format on

#endif

/** The start of a standard MRPT "try...catch()" block that allows
 * tracing throw the call stack after an exception. \sa
 * MRPT_TRY_END,MRPT_TRY_END_WITH_CLEAN_UP
 */
#define MRPT_TRY_START \
  try                  \
  {
/** The end of a standard MRPT "try...catch()" block that allows tracing
 * throw the call stack after an exception. \sa
 * MRPT_TRY_START,MRPT_TRY_END_WITH_CLEAN_UP
 */
#define MRPT_TRY_END                                        \
  }                                                         \
  catch (std::bad_alloc&) { throw; }                        \
  catch (const mrpt::ExceptionWithCallBackBase&) { throw; } \
  catch (const std::exception& __e) { throw mrpt::ExceptionWithCallBack(__e); }

/** The end of a standard MRPT "try...catch()" block that allows tracing
 * throw the call stack after an exception, including a "clean up" piece
 * of code to be run before throwing the exceptions. \sa
 * MRPT_TRY_END,MRPT_TRY_START
 *
 */
#define MRPT_TRY_END_WITH_CLEAN_UP(stuff) \
  }                                       \
  catch (std::bad_alloc&) { throw; }      \
  catch (...)                             \
  {                                       \
    {                                     \
      stuff                               \
    }                                     \
    throw;                                \
  }

// General macros for use within each MRPT method/function. They
// provide nested exception handling
#define MRPT_START                    MRPT_TRY_START
#define MRPT_END                      MRPT_TRY_END
#define MRPT_END_WITH_CLEAN_UP(stuff) MRPT_TRY_END_WITH_CLEAN_UP(stuff)

}  // namespace mrpt
