/*
 * Custom types for testing & libpqxx support those types
 */
#if !defined(PQXX_H_TEST_TYPES)
#  define PQXX_H_TEST_TYPES

#  include <pqxx/strconv>

#  include <cstdint>
#  include <cstdio>
#  include <cstring>
#  include <exception>
#  include <iomanip>
#  include <regex>
#  include <sstream>
#  include <string>
#  include <vector>


namespace pqxx
{
template<> struct nullness<std::byte> final : no_null<std::byte>
{};
} // namespace pqxx


class ipv4
{
public:
  ipv4() : m_as_int{0u} {}
  ipv4(ipv4 const &) = default;
  ipv4(ipv4 &&) = default;
  explicit ipv4(uint32_t i) : m_as_int{i} {}
  ipv4(
    unsigned char b1, unsigned char b2, unsigned char b3, unsigned char b4) :
          ipv4()
  {
    set_byte(0, b1);
    set_byte(1, b2);
    set_byte(2, b3);
    set_byte(3, b4);
  }
  ~ipv4() = default;

  ipv4 &operator=(ipv4 const &) = default;
  ipv4 &operator=(ipv4 &&) = default;

  bool operator==(ipv4 const &o) const { return m_as_int == o.m_as_int; }

  /// Index bytes, from 0 to 3, in network (i.e. Big-Endian) byte order.
  unsigned int operator[](int byte) const
  {
    if (byte < 0 or byte > 3)
      throw pqxx::usage_error("Byte out of range.");
    auto const shift = compute_shift(byte);
    return static_cast<unsigned int>((m_as_int >> shift) & 0xff);
  }

  /// Set individual byte, in network byte order.
  void set_byte(int byte, uint32_t value)
  {
    auto const shift = compute_shift(byte);
    auto const blanked = (m_as_int & ~uint32_t(0xff << shift));
    m_as_int = (blanked | ((value & 0xff) << shift));
  }

private:
  static unsigned compute_shift(int byte)
  {
    if (byte < 0 or byte > 3)
      throw pqxx::usage_error("Byte out of range.");
    return static_cast<unsigned>((3 - byte) * 8);
  }

  uint32_t m_as_int;
};


using bytea = std::vector<unsigned char>;


namespace pqxx
{
template<> struct nullness<ipv4> final : no_null<ipv4>
{};


template<> struct string_traits<ipv4> final
{
  static ipv4 from_string(std::string_view text, sl loc = sl::current())
  {
    ipv4 ts;
    if (std::data(text) == nullptr)
      internal::throw_null_conversion(name_type<ipv4>(), loc);
    std::vector<std::size_t> ends;
    for (std::size_t i{0}; i < std::size(text); ++i)
      if (text[i] == '.')
        ends.push_back(i);
    ends.push_back(std::size(text));
    if (std::size(ends) != 4)
      throw conversion_error{std::format(
        "Can't parse '{}' as ipv4: expected 4 octets, found {}.", text,
        std::size(ends))};
    std::size_t start{0};
    for (int i{0}; i < 4; ++i)
    {
      auto idx{static_cast<std::size_t>(i)};
      std::string_view const digits{&text[start], ends.at(idx) - start};
      auto value{pqxx::from_string<uint32_t>(digits)};
      ts.set_byte(i, value);
      start = ends.at(idx) + 1;
    }
    return ts;
  }

  static std::string_view
  to_buf(std::span<char> buf, ipv4 const &value, ctx c = {})
  {
    if (std::cmp_less(std::size(buf), size_buffer(value)))
      throw conversion_error{"Buffer too small for ipv4.", c.loc};

    auto tmp{buf};
    for (int i = 0; i < 4; ++i)
    {
      auto const octet{pqxx::into_buf(tmp, value[i], c)};
      // C++26: Use at().
      tmp[octet] = '.';
      tmp = tmp.subspan(octet + 1);
    }
    // Return the composed string, but leave off the trailing dot.
    return {
      std::data(buf),
      static_cast<std::size_t>(std::data(tmp) - std::data(buf)) - 1};
  }

  static constexpr std::size_t size_buffer(ipv4 const &) noexcept
  {
    return 20;
  }
};
} // namespace pqxx


namespace pqxx::test::internal
{
inline char nibble_to_hex(unsigned nibble)
{
  if (nibble < 10)
    return char('0' + nibble);
  else if (nibble < 16)
    return char('a' + (nibble - 10));
  else
    throw std::runtime_error{"Invalid digit going into bytea."};
}


inline unsigned hex_to_digit(char hex)
{
  auto x = static_cast<unsigned char>(hex);
  if (x >= '0' and x <= '9')
    return static_cast<unsigned>(x - '0');
  else if (x >= 'a' and x <= 'f')
    return static_cast<unsigned>(10 + x - 'a');
  else if (x >= 'A' and x <= 'F')
    return static_cast<unsigned>(10 + x - 'A');
  else
    throw std::runtime_error{"Invalid hex in bytea."};
}
} // namespace pqxx::test::internal


namespace pqxx
{
template<> struct nullness<bytea> final : no_null<bytea>
{};


template<> struct string_traits<bytea> final
{
  static bytea from_string(std::string_view text)
  {
    if ((std::size(text) & 1) != 0)
      throw std::runtime_error{"Odd hex size."};
    bytea value;
    value.reserve((std::size(text) - 2) / 2);
    for (std::size_t i = 2; i < std::size(text); i += 2)
    {
      auto hi = pqxx::test::internal::hex_to_digit(text[i]),
           lo = pqxx::test::internal::hex_to_digit(text[i + 1]);
      value.push_back(static_cast<unsigned char>((hi << 4) | lo));
    }
    return value;
  }

  // NOLINTNEXTLINE(readability-non-const-parameter)
  static zview to_buf(char *begin, char *end, bytea const &value)
  {
    auto const need = size_buffer(value);
    auto const have = end - begin;
    if (std::size_t(have) < need)
      throw pqxx::conversion_overrun{"Not enough space in buffer for bytea."};
    // clang-tidy rule bug:
    // NOLINTNEXTLINE(misc-const-correctness)
    char *pos = begin;
    *pos++ = '\\';
    *pos++ = 'x';
    for (unsigned char const u : value)
    {
      *pos++ = pqxx::test::internal::nibble_to_hex(unsigned(u) >> 4);
      *pos++ = pqxx::test::internal::nibble_to_hex(unsigned(u) & 0x0f);
    }
    *pos++ = '\0';
    return {begin, pos - begin - 1};
  }

  static char *into_buf(char *begin, char *end, bytea const &value)
  {
    return begin + std::size(to_buf(begin, end, value)) + 1;
  }

  static std::size_t size_buffer(bytea const &value)
  {
    return 2 + 2 * std::size(value) + 1;
  }
};
} // namespace pqxx
#endif
