#pragma once

#include <oxenc/common.h>

#include <array>
#include <cassert>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <iterator>
#include <memory>
#include <optional>
#include <span>
#include <type_traits>
#include <vector>

#include "types.hpp"

namespace session {

using namespace oxenc;

// Helper functions to convert to/from spans
template <typename OutChar = unsigned char, typename InChar>
inline std::span<const OutChar> as_span(const std::span<const InChar>& sp) {
    return {reinterpret_cast<const OutChar*>(sp.data()), sp.size()};
}

template <typename OutChar = unsigned char, oxenc::bt_input_string T>
inline std::span<const OutChar> to_span(const T& c) {
    return {reinterpret_cast<const OutChar*>(c.data()), c.size()};
}

template <typename OutChar = unsigned char, std::size_t N>
inline std::span<const OutChar> to_span(const char (&literal)[N]) {
    return {reinterpret_cast<const OutChar*>(literal), N - 1};
}

template <typename OutChar = unsigned char, typename Container>
    requires(!oxenc::bt_input_string<Container>)
inline std::span<const OutChar> to_span(const Container& c) {
    return {reinterpret_cast<const OutChar*>(c.data()), c.size()};
}

// Helper functions to convert container types
template <typename OutContainer, typename InContainer>
inline OutContainer convert(const InContainer& in) {
    using out_value_type = typename OutContainer::value_type;
    auto begin = reinterpret_cast<const out_value_type*>(in.data());
    return OutContainer(begin, begin + in.size());
}

template <typename OutChar = unsigned char, typename InChar>
inline std::vector<OutChar> to_vector(std::span<const InChar> sp) {
    return convert<std::vector<OutChar>>(sp);
}

template <typename OutChar = unsigned char, oxenc::bt_input_string T>
inline std::vector<OutChar> to_vector(const T& c) {
    return convert<std::vector<OutChar>>(to_span(c));
}

template <typename OutChar = unsigned char, typename InChar, std::size_t N>
inline std::vector<OutChar> to_vector(const std::array<InChar, N>& arr) {
    return convert<std::vector<OutChar>>(arr);
}

template <typename OutChar = unsigned char, typename Container>
    requires(!oxenc::bt_input_string<Container>)
inline std::vector<OutChar> to_vector(const Container& c) {
    return convert<std::vector<OutChar>>(to_span(c));
}

template <std::size_t N, typename InChar>
inline std::array<unsigned char, N> to_array(std::span<const InChar> sp) {
    std::array<unsigned char, N> result{};
    std::copy_n(
            reinterpret_cast<const unsigned char*>(sp.data()),
            std::min(N, sp.size()),
            result.begin());
    return result;
}

template <typename Container>
inline std::string to_string(const Container& c) {
    return convert<std::string>(c);
}

template <typename OutChar = char, typename Container>
inline std::string_view to_string_view(const Container& c) {
    return {reinterpret_cast<const OutChar*>(c.data()), c.size()};
}

// Helper function to go to/from char pointers to unsigned char pointers:
template <oxenc::basic_char Char>
inline const unsigned char* to_unsigned(const Char* x) {
    return reinterpret_cast<const unsigned char*>(x);
}
template <oxenc::basic_char Char>
inline unsigned char* to_unsigned(Char* x) {
    return reinterpret_cast<unsigned char*>(x);
}
inline const unsigned char* to_unsigned(const std::byte* x) {
    return reinterpret_cast<const unsigned char*>(x);
}
inline unsigned char* to_unsigned(std::byte* x) {
    return reinterpret_cast<unsigned char*>(x);
}
// These do nothing, but having them makes template metaprogramming easier:
inline const unsigned char* to_unsigned(const unsigned char* x) {
    return x;
}
inline unsigned char* to_unsigned(unsigned char* x) {
    return x;
}

inline uint64_t get_timestamp() {
    return std::chrono::steady_clock::now().time_since_epoch().count();
}

/// Returns true if the first string is equal to the second string, compared case-insensitively.
inline bool string_iequal(std::string_view s1, std::string_view s2) {
    return std::equal(s1.begin(), s1.end(), s2.begin(), s2.end(), [](char a, char b) {
        return std::tolower(static_cast<unsigned char>(a)) ==
               std::tolower(static_cast<unsigned char>(b));
    });
}

using uc32 = std::array<unsigned char, 32>;
using uc33 = std::array<unsigned char, 33>;
using uc64 = std::array<unsigned char, 64>;

/// Takes a container of string-like binary values and returns a vector of unsigned char spans
/// viewing those values.  This can be used on a container of any type with a `.data()` and a
/// `.size()` where `.data()` is a one-byte value pointer; std::string, std::string_view,
/// std::vector<const unsigned char>, std::span<const unsigned char>, etc. apply, as does std::array
/// of 1-byte char types.
///
/// This is useful in various libsession functions that require such a vector.  Note that the
/// returned vector's views are valid only as the original container remains alive; this is
/// typically used inline rather than stored, such as:
///
///     session::function_taking_a_view_vector(session::to_view_vector(mydata));
///
/// There are two versions of this: the first takes a generic iterator pair; the second takes a
/// single container.
template <typename It>
std::vector<std::span<const unsigned char>> to_view_vector(It begin, It end) {
    std::vector<std::span<const unsigned char>> vec;
    vec.reserve(std::distance(begin, end));
    for (; begin != end; ++begin) {
        if constexpr (std::is_same_v<std::remove_cv_t<decltype(*begin)>, char*>)  // C strings
            vec.emplace_back(*begin);
        else {
            static_assert(
                    sizeof(*begin->data()) == 1,
                    "to_view_vector can only be used with containers of string-like types of "
                    "1-byte characters");
            vec.emplace_back(reinterpret_cast<const unsigned char*>(begin->data()), begin->size());
        }
    }
    return vec;
}

template <typename Container>
std::vector<std::span<const unsigned char>> to_view_vector(const Container& c) {
    return to_view_vector(c.begin(), c.end());
}

/// Splits a string on some delimiter string and returns a vector of string_view's pointing into the
/// pieces of the original string.  The pieces are valid only as long as the original string remains
/// valid.  Leading and trailing empty substrings are not removed.  If delim is empty you get back a
/// vector of string_views each viewing one character.  If `trim` is true then leading and trailing
/// empty values will be suppressed.
///
///     auto v = split("ab--c----de", "--"); // v is {"ab", "c", "", "de"}
///     auto v = split("abc", ""); // v is {"a", "b", "c"}
///     auto v = split("abc", "c"); // v is {"ab", ""}
///     auto v = split("abc", "c", true); // v is {"ab"}
///     auto v = split("-a--b--", "-"); // v is {"", "a", "", "b", "", ""}
///     auto v = split("-a--b--", "-", true); // v is {"a", "", "b"}
///
std::vector<std::string_view> split(
        std::string_view str, std::string_view delim, bool trim = false);

/// Returns protocol, host, port, path.  Port can be empty; throws on unparseable values.  protocol
/// and host get normalized to lower-case.  Port will be null if not present in the URL, or if set
/// to the default for the protocol.  Path can be empty (a single optional `/` after the domain will
/// be ignored).
std::tuple<std::string, std::string, std::optional<uint16_t>, std::optional<std::string>> parse_url(
        std::string_view url);

/// Truncates a utf-8 encoded string to at most `n` bytes long, but with care as to not truncate in
/// the middle of a unicode codepoint.  If the `n` length would shorten the string such that it
/// terminates in the middle of a utf-8 encoded unicode codepoint then the string is shortened
/// further to not include the sliced unicode codepoint.
///
/// For example, "happy 🎂🎂🎂!!" in utf8 encoding is 20 bytes long:
/// "happy \xf0\x9f\x8e\x82\xf0\x9f\x8e\x82\xf0\x9f\x8e\x82!!", that is:
/// - "happy " (6 bytes)
/// - 🎂 = 0xf0 0x9f 0x8e 0x82 (12 bytes = 3 × 4 bytes each)
/// - "!!" (2 bytes)
/// Truncating this to different lengths results in:
/// - 20, 21, or higher - the 20-byte full string
/// - 19: "happy 🎂🎂🎂!"
/// - 18: "happy 🎂🎂🎂"
/// - 17: "happy 🎂🎂" (14 bytes)
/// - 16, 15, 14: same result as 17
/// - 13, 12, 11, 10: "happy 🎂"
/// - 9, 8, 7, 6: "happy "
/// - 5: "happy"
/// - 4: "happ"
/// - 3: "hap"
/// - 2: "ha"
/// - 1: "a"
/// - 0: ""
///
/// This function is *not* (currently) aware of unicode "characters", but merely codepoints (because
/// grapheme clusters get incredibly complicated).  This is only designed to prevent invalid utf8
/// encodings.  For example, the pair 🇦🇺 (REGIONAL INDICATOR SYMBOL LETTER A, REGIONAL INDICATOR
/// SYMBOL LETTER U) is often rendered as a single Australian flag, but could get chopped here into
/// just 🇦 (REGIONAL INDICATOR SYMBOL LETTER A) rather than removing the getting split in the middle
/// of the pair, which would show up as a decorated A rather than an Australian flag.  Another
/// example, é (LATIN SMALL LETTER E, COMBINING ACUTE ACCENT) could get chopped between the e and
/// the accent modifier, and end up as just "e" in the truncated string.
///
inline std::string utf8_truncate(std::string val, size_t n) {
    if (val.size() <= n)
        return val;
    // The *first* char in a utf8 sequence is either:
    // 0b0....... -- single byte encoding, for values up to 0x7f (ascii)
    // 0b11...... -- multi-byte encoding for values >= 0x80; the number of sequential high bit 1's
    // in the first character indicate the sequence length (e.g. 0b1110.... starts a 3-byte
    // sequence).  In our birthday cake encoding, the first byte is \xf0 == 0b11110000, and so it is
    // a 4-byte sequence.
    //
    // That leaves 0x10...... bytes as continuation bytes, each one holding 6 bits of the unicode
    // codepoint, in big endian order, so our birthday cake (in bits): 0b11110000 0b10011111
    // 0b10001110 0b10000010 is the unicode value 0b000 011111 001110 000010 == 0x1f382 == U+1F382:
    // BIRTHDAY CAKE).
    //
    // To prevent slicing, then, we just have to ensure the the first byte after the slice point is
    // *not* a continuation byte (and therefore is either a plain ascii character codepoint, or is
    // the start of a multi-character codepoint).
    while (n > 0 && (val[n] & 0b1100'0000) == 0b1000'0000)
        --n;

    val.resize(n);
    return val;
}

// Helper function to transform a timestamp provided in seconds, milliseconds or microseconds to
// seconds
inline int64_t to_epoch_seconds(int64_t timestamp) {
    return timestamp > 9'000'000'000'000 ? timestamp / 1'000'000
         : timestamp > 9'000'000'000     ? timestamp / 1'000
                                         : timestamp;
}

}  // namespace session
