#pragma once

#include <cstddef>
#include <cstdint>
#include <string>
#include <vector>

#include <tl/optional.hpp>

// Compile-time lookup table for decoding hex characters to their 0-15 value,
// or -1 for invalid characters.
struct HexDecodeTable
{
    std::int8_t values[256];

    constexpr HexDecodeTable() : values{}
    {
        for (auto &v : values) {
            v = -1;
        }
        for (char c = '0'; c <= '9'; ++c) {
            values[static_cast<std::size_t>(c)] = static_cast<std::int8_t>(c - '0');
        }
        for (char c = 'a'; c <= 'f'; ++c) {
            values[static_cast<std::size_t>(c)] = static_cast<std::int8_t>(c - 'a' + 10);
        }
        for (char c = 'A'; c <= 'F'; ++c) {
            values[static_cast<std::size_t>(c)] = static_cast<std::int8_t>(c - 'A' + 10);
        }
    }
};

namespace
{
const HexDecodeTable kHexDecode;
}  // namespace

inline std::string hex_encode(const void *data, std::size_t len)
{
    const auto *bytes = static_cast<const std::uint8_t *>(data);
    static constexpr char kHexChars[] = "0123456789abcdef";
    std::string result;
    result.reserve(len * 2U);
    for (std::size_t i = 0; i < len; ++i) {
        result.push_back(kHexChars[(bytes[i] >> 4U) & 0x0FU]);
        result.push_back(kHexChars[bytes[i] & 0x0FU]);
    }
    return result;
}

inline std::string hex_encode(const std::string &data) { return hex_encode(data.data(), data.size()); }

inline std::string hex_encode(const std::vector<std::uint8_t> &data) { return hex_encode(data.data(), data.size()); }

// Returns decoded bytes, or tl::nullopt on invalid input (odd length or
// non-hex characters).
inline tl::optional<std::vector<std::uint8_t>> hex_decode(const std::string &hex)
{
    if (hex.size() % 2U != 0U) {
        return tl::nullopt;
    }
    std::vector<std::uint8_t> result;
    result.reserve(hex.size() / 2U);
    for (std::size_t i = 0; i < hex.size(); i += 2U) {
        auto high = kHexDecode.values[static_cast<std::uint8_t>(hex[i])];
        auto low = kHexDecode.values[static_cast<std::uint8_t>(hex[i + 1U])];
        if (high < 0 || low < 0) {
            return tl::nullopt;
        }
        result.push_back(static_cast<std::uint8_t>((high << 4) | low));
    }
    return result;
}
