////////////////////////////////////////////////////////////
//
// SFML - Simple and Fast Multimedia Library
// Copyright (C) 2007-2026 Laurent Gomila (laurent@sfml-dev.org)
//
// This software is provided 'as-is', without any express or implied warranty.
// In no event will the authors be held liable for any damages arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it freely,
// subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented;
//    you must not claim that you wrote the original software.
//    If you use this software in a product, an acknowledgment
//    in the product documentation would be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such,
//    and must not be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//
////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////
// Headers
////////////////////////////////////////////////////////////
#include <SFML/Network/IpAddress.hpp>
#include <SFML/Network/Packet.hpp>
#include <SFML/Network/SocketImpl.hpp>
#include <SFML/Network/SocketSelector.hpp>
#include <SFML/Network/TcpSocket.hpp>

#include <SFML/System/Err.hpp>
#include <SFML/System/String.hpp>
#include <SFML/System/Version.hpp>

#include <mbedtls/version.h>
#if (MBEDTLS_VERSION_MAJOR < 4)
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/entropy.h>
#endif
#include <mbedtls/error.h>
#include <mbedtls/net_sockets.h>
#include <mbedtls/ssl.h>

#if defined(SFML_SYSTEM_WINDOWS)
#include <wincrypt.h>
#elif defined(SFML_SYSTEM_MACOS)
#include <Availability.h>
#include <Security/Security.h>

#include <CoreFoundation/CoreFoundation.h>
#endif

#include <algorithm>
#include <array>
#include <filesystem>
#include <mutex>
#include <optional>
#include <ostream>

#include <cassert>
#include <cstring>

#ifdef _MSC_VER
#pragma warning(disable : 4127) // "conditional expression is constant" generated by the FD_SET macro
#endif


namespace
{
// Low-level send/receive flags (OS-dependent)
#ifdef SFML_SYSTEM_LINUX
constexpr int flags = MSG_NOSIGNAL;
#else
constexpr int flags = 0;
#endif

std::string tlsErrorString(int errnum)
{
    std::array<char, 1024> buffer{};
    mbedtls_strerror(errnum, buffer.data(), buffer.size());
    return buffer.data();
}

bool loadSystemCertificates([[maybe_unused]] mbedtls_x509_crt* x509crt, [[maybe_unused]] mbedtls_x509_crl* x509crl)
{
#if defined(SFML_SYSTEM_WINDOWS)
    static const auto getErrorString = [](DWORD error) -> std::string
    {
        PTCHAR buffer = nullptr;
        if (FormatMessage(FORMAT_MESSAGE_MAX_WIDTH_MASK | FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
                          nullptr,
                          error,
                          0,
                          reinterpret_cast<PTCHAR>(&buffer),
                          0,
                          nullptr) == 0)
        {
            return "Unknown error.";
        }

        const sf::String message = buffer;
        LocalFree(buffer);
        return message.toAnsiString();
    };

    const auto loadStore = [&](const char* store)
    {
        auto* systemStore = CertOpenSystemStoreA(0, store);

        if (systemStore == nullptr)
        {
            sf::err() << "Failed to open Windows certificate store: " << getErrorString(GetLastError()) << std::endl;
            return false;
        }

        const CERT_CONTEXT* cert{};
        cert = CertEnumCertificatesInStore(systemStore, cert);

        while (cert)
        {
            if (cert->dwCertEncodingType == X509_ASN_ENCODING)
            {
                // Certificate parsing might fail because a certificate makes use of intentionally removed features
                // We can just ignore the certificate and move on to the next one
                mbedtls_x509_crt_parse(x509crt, cert->pbCertEncoded, cert->cbCertEncoded);
            }

            cert = CertEnumCertificatesInStore(systemStore, cert);
        }

        const CRL_CONTEXT* crl{};
        crl = CertEnumCRLsInStore(systemStore, crl);

        while (crl)
        {
            if (crl->dwCertEncodingType == X509_ASN_ENCODING)
            {
                // CRL parsing might fail because a CRL makes use of intentionally removed features
                // We can just ignore the CRL and move on to the next one
                mbedtls_x509_crl_parse(x509crl, crl->pbCrlEncoded, crl->cbCrlEncoded);
            }

            crl = CertEnumCRLsInStore(systemStore, crl);
        }

        if (!CertCloseStore(systemStore, 0))
            sf::err() << "Failed to close Windows certificate store: " << getErrorString(GetLastError()) << std::endl;

        return true;
    };

    return loadStore("ROOT") && loadStore("CA");
#elif (defined(SFML_SYSTEM_LINUX) || defined(SFML_SYSTEM_ANDROID) || defined(SFML_SYSTEM_FREEBSD) || \
       defined(SFML_SYSTEM_OPENBSD) || defined(SFML_SYSTEM_NETBSD))
#if defined(SFML_CA_PATH)
    if (!std::filesystem::exists(SFML_CA_PATH))
    {
        sf::err() << "Failed to load CA certificates: " << SFML_CA_PATH << " does not exist" << std::endl;
        return false;
    }

    int result{};

    if (std::filesystem::is_directory(SFML_CA_PATH))
    {
        result = mbedtls_x509_crt_parse_path(x509crt, SFML_CA_PATH);
    }
    else
    {
        result = mbedtls_x509_crt_parse_file(x509crt, SFML_CA_PATH);
    }

    if (result < 0)
    {
        sf::err() << "Failed to load CA certificates from " << SFML_CA_PATH << ": " << tlsErrorString(result) << std::endl;
        return false;
    }
    else if (result > 0)
    {
        sf::err() << result << " certificates failed to load from " << SFML_CA_PATH << std::endl;
    }

    return true;
#else
    auto loadStore = [&](const char* path)
    {
        // Just trying to load all known paths is simpler than specifying paths per distribution
        if (!std::filesystem::exists(path))
            return true;

        if (auto result = mbedtls_x509_crt_parse_path(x509crt, path); result < 0)
        {
            sf::err() << "Failed to load CA certificate directory: " << tlsErrorString(result) << std::endl;
            return false;
        }

        return true;
    };

    // TODO: Handle revocation directory as well

#if defined(SFML_SYSTEM_LINUX)
    return loadStore("/etc/ssl/") && loadStore("/etc/ssl/certs/") && loadStore("/etc/pki/ca-trust/extracted/pem/") &&
           loadStore("/etc/pki/tls/") && loadStore("/etc/pki/tls/certs/");
#elif defined(SFML_SYSTEM_ANDROID)
    return loadStore("/system/etc/security/cacerts/") && loadStore("/data/misc/keychain/cacerts-added/");
#elif defined(SFML_SYSTEM_FREEBSD)
    return loadStore("/usr/local/share/certs");
#elif defined(SFML_SYSTEM_OPENBSD)
    return loadStore("/etc/ssl/");
#elif defined(SFML_SYSTEM_NETBSD)
    return loadStore("/etc/openssl/certs");
#endif
#endif
#elif defined(SFML_SYSTEM_MACOS)
    auto loadStore = [&](SecTrustSettingsDomain domain)
    {
        static constexpr auto osStatusErrorString = [](OSStatus status)
        {
            CFStringRef stringRef = SecCopyErrorMessageString(status, nullptr);
            std::string string(CFStringGetCStringPtr(stringRef, kCFStringEncodingUTF8));
            CFRelease(stringRef);
            return string;
        };

        CFArrayRef     certs{};
        const OSStatus status = SecTrustSettingsCopyCertificates(domain, &certs);

        if (status == errSecNoTrustSettings)
            return true;

        if (status != errSecSuccess)
        {
            sf::err() << "Failed to load system certificates: " << osStatusErrorString(status);
            return false;
        }

        for (auto i = 0; i < CFArrayGetCount(certs); ++i)
        {
            const auto* cert = CFArrayGetValueAtIndex(certs, i);
            CFDataRef   der{};

            if (auto result = SecItemExport(cert, kSecFormatX509Cert, 0, nullptr, &der); result != errSecSuccess)
            {
                CFRelease(der);
                CFRelease(certs);
                sf::err() << "Failed to load system certificate: " << osStatusErrorString(result);
                return false;
            }

            // Certificate parsing might fail because a certificate makes use of intentionally removed features
            // We can just ignore the certificate and move on to the next one
            mbedtls_x509_crt_parse(x509crt,
                                   reinterpret_cast<const unsigned char*>(CFDataGetBytePtr(der)),
                                   static_cast<std::size_t>(CFDataGetLength(der)));

            CFRelease(der);
        }

        return true;
    };

    return (loadStore(kSecTrustSettingsDomainUser) && loadStore(kSecTrustSettingsDomainAdmin) &&
            loadStore(kSecTrustSettingsDomainSystem));
#else
    // Add implementations for other system certificate stores here
    return false;
#endif
}

// When building MbedTLS ourselves, it doesn't provide its own built-in cross-platform mutex implementation
// Instead it delegates mutex management to our code with the use of the following callbacks
// We just provide wrapper functions around std::mutex
#if !defined(MBEDTLS_THREADING_C)
#error "Mbed TLS not built with threading support however it is required"
#endif

struct MbedTlsThreading
{
#if defined(MBEDTLS_THREADING_ALT)
    MbedTlsThreading()
    {
        mbedtls_threading_set_alt(
            [](mbedtls_threading_mutex_t* ptr) // Construction
            {
                if (!ptr)
                    return;
                *ptr = std::make_unique<std::mutex>().release();
            },
            [](mbedtls_threading_mutex_t* ptr) // Destruction
            {
                if (!ptr)
                    return;
                [[maybe_unused]] const std::unique_ptr<std::mutex> dummy(static_cast<std::mutex*>(*ptr));
            },
            [](mbedtls_threading_mutex_t* ptr) // Lock
            {
                if (!ptr)
                    return MBEDTLS_ERR_THREADING_BAD_INPUT_DATA;
                try
                {
                    static_cast<std::mutex*>(*ptr)->lock();
                } catch (const std::exception&)
                {
                    return MBEDTLS_ERR_THREADING_MUTEX_ERROR;
                }
                return 0;
            },
            [](mbedtls_threading_mutex_t* ptr) // Unlock
            {
                if (!ptr)
                    return MBEDTLS_ERR_THREADING_BAD_INPUT_DATA;
                try
                {
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 26110) // Caller failing to hold lock before calling function
#endif
                    static_cast<std::mutex*>(*ptr)->unlock();
#ifdef _MSC_VER
#pragma warning(pop)
#endif
                } catch (const std::exception&)
                {
                    return MBEDTLS_ERR_THREADING_MUTEX_ERROR;
                }
                return 0;
            });
    }

    ~MbedTlsThreading()
    {
        mbedtls_threading_free_alt();
    }
#else
#if !defined(MBEDTLS_THREADING_PTHREAD)
#error "No Mbed TLS threading implementation available"
#endif
#endif
};

struct MbedTlsSharedState
{
    MbedTlsSharedState()
    {
#if (MBEDTLS_VERSION_MAJOR < 4)
        // Initialize crypto library in Mbed TLS 3.x.x and earlier
        mbedtls_entropy_init(&entropyContext);
        mbedtls_ctr_drbg_init(&ctrDrbgContext);

        // We use a personalization string as a cheap way to minimally seed the RNG in case entropy is low
        static const auto personalizationString = "sfml-network-" + std::string(sf::version().string) +
                                                  "-mbedtls-" MBEDTLS_VERSION_STRING_FULL;

        if (auto result = mbedtls_ctr_drbg_seed(&ctrDrbgContext,
                                                mbedtls_entropy_func,
                                                &entropyContext,
                                                reinterpret_cast<const unsigned char*>(personalizationString.data()),
                                                personalizationString.size());
            result != 0)
        {
            sf::err() << "Failed to seed DRBG: " << tlsErrorString(result) << std::endl;
            assert(false && "Failed to seed DRBG");
            throw std::runtime_error("Failed to seed DRBG");
        }
#else
        // Initialize crypto library in Mbed TLS 4.x.x and later
        if (auto result = psa_crypto_init(); result != PSA_SUCCESS)
        {
            sf::err() << "Failed to initialize crypto library" << std::endl;
            assert(false && "Failed to initialize crypto library");
            throw std::runtime_error("Failed to initialize crypto library");
        }
#endif
    }

#if (MBEDTLS_VERSION_MAJOR < 4)
    ~MbedTlsSharedState()
    {
        mbedtls_ctr_drbg_free(&ctrDrbgContext);
        mbedtls_entropy_free(&entropyContext);
    }
#endif

    MbedTlsThreading threading;

#if (MBEDTLS_VERSION_MAJOR < 4)
    mbedtls_entropy_context  entropyContext{};
    mbedtls_ctr_drbg_context ctrDrbgContext{};
#endif
};

[[maybe_unused]] MbedTlsSharedState mbedTlsSharedState;
} // namespace

namespace sf
{
struct TcpSocket::Impl
{
    TcpSocket::TlsStatus setupTls(
        TcpSocket&        socket,
        const sf::String& hostname,
        bool              verifyPeer,
        const std::byte*  certificateChainData,
        std::size_t       certificateChainSize,
        const std::byte*  privateKeyData,
        std::size_t       privateKeySize,
        const std::byte*  privateKeyPasswordData,
        std::size_t       privateKeyPasswordSize)
    {
        // We can't set up TLS if the underlying TCP stream isn't connected yet
        if (!socket.getRemoteAddress().has_value())
            return TlsStatus::NotConnected;

        if (!tlsState.has_value())
        {
            // Construct new TLS state
            auto& state = tlsState.emplace();

            // Load user-provided certificate data
            if (certificateChainData != nullptr && certificateChainSize > 0)
            {
                if (auto result = mbedtls_x509_crt_parse(&state.x509Crt,
                                                         reinterpret_cast<const unsigned char*>(certificateChainData),
                                                         certificateChainSize);
                    result != 0)
                {
                    if (result < 0)
                    {
                        err() << "Failed to load provided certificate chain: " << tlsErrorString(result) << std::endl;
                    }
                    else if (result == 1)
                    {
                        err() << "Only 1 certificate could be loaded from provided certificate chain" << std::endl;
                    }
                    else
                    {
                        err() << "Only " << result << " certificates could be loaded from provided certificate chain"
                              << std::endl;
                    }

                    tlsState.reset();
                    return TlsStatus::Error;
                }
            }

            const auto isServer = privateKeyData != nullptr;

            // If we are a client, load certificates from the system certificate store
            if (!isServer)
            {
                if (auto result = loadSystemCertificates(&state.x509Crt, &state.x509Crl); !result)
                {
                    err() << "Failed to load system certificates" << std::endl;
                    tlsState.reset();
                    return TlsStatus::Error;
                }
            }

            // If we are a server, load private key
            if (privateKeyData != nullptr && privateKeySize > 0)
            {
                // Passing in an RNG is only required in Mbed TLS 3.x.x, neither 2.x.x or 4.x.x requires it
#if MBEDTLS_VERSION_MAJOR == 3
                auto result = mbedtls_pk_parse_key(&state.privateKeyContext,
                                                   reinterpret_cast<const unsigned char*>(privateKeyData),
                                                   privateKeySize,
                                                   reinterpret_cast<const unsigned char*>(privateKeyPasswordData),
                                                   privateKeyPasswordSize,
                                                   mbedtls_ctr_drbg_random,
                                                   &mbedTlsSharedState.ctrDrbgContext);
#else
                auto result = mbedtls_pk_parse_key(&state.privateKeyContext,
                                                   reinterpret_cast<const unsigned char*>(privateKeyData),
                                                   privateKeySize,
                                                   reinterpret_cast<const unsigned char*>(privateKeyPasswordData),
                                                   privateKeyPasswordSize);
#endif

                if (result != 0)
                {
                    err() << "Failed to load provided private key: " << tlsErrorString(result) << std::endl;
                    tlsState.reset();
                    return TlsStatus::Error;
                }
            }

            if (auto result = mbedtls_ssl_config_defaults(&state.sslConfig,
                                                          isServer ? MBEDTLS_SSL_IS_SERVER : MBEDTLS_SSL_IS_CLIENT,
                                                          MBEDTLS_SSL_TRANSPORT_STREAM,
                                                          MBEDTLS_SSL_PRESET_DEFAULT);
                result != 0)
            {
                err() << "Failed to set up TLS: " << tlsErrorString(result) << std::endl;
                tlsState.reset();
                return TlsStatus::Error;
            }

            // Setting up random number generator is only required before Mbed TLS 4.x.x
#if (MBEDTLS_VERSION_MAJOR < 4)
            mbedtls_ssl_conf_rng(&state.sslConfig, mbedtls_ctr_drbg_random, &mbedTlsSharedState.ctrDrbgContext);
#endif

            // Set up peer verification mode
            mbedtls_ssl_conf_authmode(&state.sslConfig, verifyPeer ? MBEDTLS_SSL_VERIFY_REQUIRED : MBEDTLS_SSL_VERIFY_NONE);

            // Set the CA chain to use for verification
            // Set our own certificate if we are a server
            if (isServer)
            {
                if (state.x509Crt.next != nullptr)
                    mbedtls_ssl_conf_ca_chain(&state.sslConfig, state.x509Crt.next, nullptr);

                if (auto result = mbedtls_ssl_conf_own_cert(&state.sslConfig, &state.x509Crt, &state.privateKeyContext);
                    result != 0)
                {
                    err() << "Failed to load server certificate: " << tlsErrorString(result) << std::endl;
                    tlsState.reset();
                    return TlsStatus::Error;
                }
            }
            else
            {
                // If we are a client, make use of the CRL as well
                mbedtls_ssl_conf_ca_chain(&state.sslConfig, &state.x509Crt, &state.x509Crl);
            }

            if (auto result = mbedtls_ssl_setup(&state.sslContext, &state.sslConfig); result != 0)
            {
                err() << "Failed to set up TLS: " << tlsErrorString(result) << std::endl;
                tlsState.reset();
                return TlsStatus::Error;
            }

            if (!isServer)
            {
                // Set the hostname that is used for peer verification and sent via SNI if it is supported
                if (auto result = mbedtls_ssl_set_hostname(&state.sslContext,
                                                           reinterpret_cast<const char*>(hostname.toUtf8().c_str()));
                    result != 0)
                {
                    err() << "Failed to set up TLS: " << tlsErrorString(result) << std::endl;
                    tlsState.reset();
                    return TlsStatus::Error;
                }
            }

            // Set up how the TLS implementation communicates with the underlying socket
            state.netContext.fd = static_cast<int>(socket.getNativeHandle());

            mbedtls_ssl_set_bio(
                &state.sslContext,
                &socket,
                [](void* context, const unsigned char* data, std::size_t size) -> int
                {
                    auto& tcpSocket = *static_cast<TcpSocket*>(context);

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wuseless-cast"
                    const auto result = static_cast<int>(
                        ::send(tcpSocket.getNativeHandle(),
                               reinterpret_cast<const char*>(data),
                               static_cast<priv::SocketImpl::Size>(size),
                               flags));

                    if ((result == -1) && (priv::SocketImpl::getErrorStatus() == sf::Socket::Status::NotReady))
                        return MBEDTLS_ERR_SSL_WANT_WRITE;

                    return result;
#pragma GCC diagnostic pop
                },
                [](void* context, unsigned char* data, std::size_t size) -> int
                {
                    auto& tcpSocket = *static_cast<TcpSocket*>(context);

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wuseless-cast"
                    const auto result = static_cast<int>(
                        recv(tcpSocket.getNativeHandle(),
                             reinterpret_cast<char*>(data),
                             static_cast<priv::SocketImpl::Size>(size),
                             flags));

                    if ((result == -1) && (priv::SocketImpl::getErrorStatus() == sf::Socket::Status::NotReady))
                        return MBEDTLS_ERR_SSL_WANT_READ;

                    return result;
#pragma GCC diagnostic pop
                },
                nullptr);
        }

        auto& state = *tlsState;

        // Perform the TLS handshake if it isn't complete yet
        if (!state.handshakeComplete)
        {
            if (auto result = mbedtls_ssl_handshake(&state.sslContext); result != 0)
            {
                if (result == MBEDTLS_ERR_SSL_WANT_READ || result == MBEDTLS_ERR_SSL_WANT_WRITE)
                    return TlsStatus::HandshakeStarted;

#if defined(MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET)
                if (result == MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET)
                    return TlsStatus::HandshakeStarted;
#endif

                // Output the reason for verification failure
                if (result == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED)
                {
                    auto verifyResult = mbedtls_ssl_get_verify_result(&state.sslContext);

                    std::string errors;

                    if (verifyResult & MBEDTLS_X509_BADCERT_EXPIRED)
                        errors += "expired, ";
                    if (verifyResult & MBEDTLS_X509_BADCERT_REVOKED)
                        errors += "revoked, ";
                    if (verifyResult & MBEDTLS_X509_BADCERT_CN_MISMATCH)
                        errors += "CN mismatch, ";
                    if (verifyResult & MBEDTLS_X509_BADCERT_NOT_TRUSTED)
                        errors += "not trusted, ";
                    if (verifyResult & MBEDTLS_X509_BADCRL_NOT_TRUSTED)
                        errors += "CRL not trusted, ";
                    if (verifyResult & MBEDTLS_X509_BADCRL_EXPIRED)
                        errors += "CRL expired, ";
                    if (verifyResult & MBEDTLS_X509_BADCERT_MISSING)
                        errors += "missing, ";
                    if (verifyResult & MBEDTLS_X509_BADCERT_SKIP_VERIFY)
                        errors += "skip verify, ";
                    if (verifyResult & MBEDTLS_X509_BADCERT_OTHER)
                        errors += "other, ";
                    if (verifyResult & MBEDTLS_X509_BADCERT_FUTURE)
                        errors += "future, ";
                    if (verifyResult & MBEDTLS_X509_BADCRL_FUTURE)
                        errors += "CRL future, ";
                    if (verifyResult & MBEDTLS_X509_BADCERT_KEY_USAGE)
                        errors += "key usage, ";
                    if (verifyResult & MBEDTLS_X509_BADCERT_EXT_KEY_USAGE)
                        errors += "ext key usage, ";
                    if (verifyResult & MBEDTLS_X509_BADCERT_NS_CERT_TYPE)
                        errors += "NS cert type, ";
                    if (verifyResult & MBEDTLS_X509_BADCERT_BAD_MD)
                        errors += "bad MD, ";
                    if (verifyResult & MBEDTLS_X509_BADCERT_BAD_PK)
                        errors += "bad PK, ";
                    if (verifyResult & MBEDTLS_X509_BADCERT_BAD_KEY)
                        errors += "bad key, ";
                    if (verifyResult & MBEDTLS_X509_BADCRL_BAD_MD)
                        errors += "CRL bad MD, ";
                    if (verifyResult & MBEDTLS_X509_BADCRL_BAD_KEY)
                        errors += "CRL bad key, ";

                    if (errors.empty())
                    {
                        err() << "TLS certificate verification failed" << std::endl;
                    }
                    else
                    {
                        errors.resize(errors.size() - 2);
                        err() << "TLS certificate verification failed: " << errors << std::endl;
                    }
                }
                else
                {
                    err() << "TLS handshake failed: " << tlsErrorString(result) << std::endl;
                }

                tlsState.reset();
                return TlsStatus::Error;
            }

            state.handshakeComplete = true;
        }

        return TlsStatus::HandshakeComplete;
    }

    struct TlsState
    {
        TlsState()
        {
            mbedtls_ssl_init(&sslContext);
            mbedtls_ssl_config_init(&sslConfig);
            mbedtls_x509_crt_init(&x509Crt);
            mbedtls_x509_crl_init(&x509Crl);
            mbedtls_pk_init(&privateKeyContext);
        }

        ~TlsState()
        {
            mbedtls_pk_free(&privateKeyContext);
            mbedtls_x509_crl_free(&x509Crl);
            mbedtls_x509_crt_free(&x509Crt);
            mbedtls_ssl_config_free(&sslConfig);
            mbedtls_ssl_free(&sslContext);
        }

        bool                handshakeComplete{};
        mbedtls_net_context netContext{-1};
        mbedtls_ssl_context sslContext{};
        mbedtls_ssl_config  sslConfig{};
        mbedtls_x509_crt    x509Crt{};
        mbedtls_x509_crl    x509Crl{};
        mbedtls_pk_context  privateKeyContext{};
    };

    std::optional<TlsState> tlsState;
};


////////////////////////////////////////////////////////////
TcpSocket::TcpSocket() : Socket(Type::Tcp), m_impl(std::make_unique<Impl>())
{
}


////////////////////////////////////////////////////////////
TcpSocket::~TcpSocket() = default;


////////////////////////////////////////////////////////////
TcpSocket::TcpSocket(TcpSocket&&) noexcept = default;


////////////////////////////////////////////////////////////
TcpSocket& TcpSocket::operator=(TcpSocket&&) noexcept = default;


////////////////////////////////////////////////////////////
unsigned short TcpSocket::getLocalPort() const
{
    if (getNativeHandle() != priv::SocketImpl::invalidSocket())
    {
        // Retrieve information about the local end of the socket
        sockaddr_in6                 addressV6{};
        priv::SocketImpl::AddrLength size = sizeof(addressV6);
        if (getsockname(getNativeHandle(), reinterpret_cast<sockaddr*>(&addressV6), &size) != -1)
        {
            if (addressV6.sin6_family == PF_INET6)
                return ntohs(addressV6.sin6_port);

            if (addressV6.sin6_family == PF_INET)
            {
                sockaddr_in addressV4{};
                std::memcpy(&addressV4, &addressV6, sizeof(addressV4));
                return ntohs(addressV4.sin_port);
            }
        }
    }

    // We failed to retrieve the port
    return 0;
}


////////////////////////////////////////////////////////////
std::optional<IpAddress> TcpSocket::getRemoteAddress() const
{
    if (getNativeHandle() != priv::SocketImpl::invalidSocket())
    {
        // Get the address family of the socket
        sockaddr_in6                 address{};
        priv::SocketImpl::AddrLength size = sizeof(address);

        if (getsockname(getNativeHandle(), reinterpret_cast<sockaddr*>(&address), &size) == -1)
            return std::nullopt;

        // Retrieve information about the remote end of the socket
        if (address.sin6_family == PF_INET)
        {
            sockaddr_in                  addressV4{};
            priv::SocketImpl::AddrLength sizeV4 = sizeof(address);
            if (getpeername(getNativeHandle(), reinterpret_cast<sockaddr*>(&addressV4), &sizeV4) != -1)
                return IpAddress(ntohl(addressV4.sin_addr.s_addr));
        }
        else if (address.sin6_family == PF_INET6)
        {
            sockaddr_in6                 addressV6{};
            priv::SocketImpl::AddrLength sizeV6 = sizeof(addressV6);
            if (getpeername(getNativeHandle(), reinterpret_cast<sockaddr*>(&addressV6), &sizeV6) != -1)
            {
                return IpAddress(
                    {addressV6.sin6_addr.s6_addr[0],
                     addressV6.sin6_addr.s6_addr[1],
                     addressV6.sin6_addr.s6_addr[2],
                     addressV6.sin6_addr.s6_addr[3],
                     addressV6.sin6_addr.s6_addr[4],
                     addressV6.sin6_addr.s6_addr[5],
                     addressV6.sin6_addr.s6_addr[6],
                     addressV6.sin6_addr.s6_addr[7],
                     addressV6.sin6_addr.s6_addr[8],
                     addressV6.sin6_addr.s6_addr[9],
                     addressV6.sin6_addr.s6_addr[10],
                     addressV6.sin6_addr.s6_addr[11],
                     addressV6.sin6_addr.s6_addr[12],
                     addressV6.sin6_addr.s6_addr[13],
                     addressV6.sin6_addr.s6_addr[14],
                     addressV6.sin6_addr.s6_addr[15]});
            }
        }
    }

    // We failed to retrieve the address
    return std::nullopt;
}


////////////////////////////////////////////////////////////
unsigned short TcpSocket::getRemotePort() const
{
    if (getNativeHandle() != priv::SocketImpl::invalidSocket())
    {
        // Retrieve information about the remote end of the socket
        sockaddr_in6                 addressV6{};
        priv::SocketImpl::AddrLength size = sizeof(addressV6);
        if (getpeername(getNativeHandle(), reinterpret_cast<sockaddr*>(&addressV6), &size) != -1)
        {
            if (addressV6.sin6_family == PF_INET6)
                return ntohs(addressV6.sin6_port);

            if (addressV6.sin6_family == PF_INET)
            {
                sockaddr_in addressV4{};
                std::memcpy(&addressV4, &addressV6, sizeof(addressV4));
                return ntohs(addressV4.sin_port);
            }
        }
    }

    // We failed to retrieve the port
    return 0;
}


////////////////////////////////////////////////////////////
Socket::Status TcpSocket::connect(IpAddress remoteAddress, unsigned short remotePort, Time timeout)
{
    // Disconnect the socket if it is already connected
    disconnect();

    // Create the remote address
    sockaddr_in                  addressV4{};
    sockaddr_in6                 addressV6{};
    sockaddr*                    sockaddrPtr{};
    priv::SocketImpl::AddrLength sockaddrSize{};

    if (remoteAddress.isV4())
    {
        addressV4    = priv::SocketImpl::createAddress(remoteAddress.toInteger(), remotePort);
        sockaddrPtr  = reinterpret_cast<sockaddr*>(&addressV4);
        sockaddrSize = sizeof(addressV4);

        // Create the internal socket if it doesn't exist
        create(IpAddress::Type::IpV4);
    }
    else if (remoteAddress.isV6())
    {
        addressV6    = priv::SocketImpl::createAddress(remoteAddress.toBytes(), remotePort);
        sockaddrPtr  = reinterpret_cast<sockaddr*>(&addressV6);
        sockaddrSize = sizeof(addressV6);

        // Create the internal socket if it doesn't exist
        create(IpAddress::Type::IpV6);
    }

    assert(sockaddrPtr && sockaddrSize);

    if (timeout <= Time::Zero)
    {
        // ----- We're not using a timeout: just try to connect -----

        // Connect the socket
        if (::connect(getNativeHandle(), sockaddrPtr, sockaddrSize) == -1)
            return priv::SocketImpl::getErrorStatus();

        // Connection succeeded
        return Status::Done;
    }

    // ----- We're using a timeout: we'll need a few tricks to make it work -----

    // Save the previous blocking state
    const bool blocking = isBlocking();

    // Switch to non-blocking to enable our connection timeout
    if (blocking)
        setBlocking(false);

    // Try to connect to the remote address
    if (::connect(getNativeHandle(), sockaddrPtr, sockaddrSize) >= 0)
    {
        // We got instantly connected! (it may no happen a lot...)
        setBlocking(blocking);
        return Status::Done;
    }

    // Get the error status
    Status status = priv::SocketImpl::getErrorStatus();

    // If we were in non-blocking mode, return immediately
    if (!blocking)
        return status;

    // Otherwise, wait until something happens to our socket (success, timeout or error)
    if (status == Socket::Status::NotReady)
    {
        // Setup the selector
        SocketSelector selector;
        selector.add(*this, SocketSelector::Send);

        // Wait for our socket to become ready to send (which means that the connection request has returned)
        if (selector.wait(timeout))
        {
            // At this point the connection may have been either accepted or refused.
            // To know whether it's a success or a failure, we must check the address of the connected peer
            if (getRemoteAddress().has_value())
            {
                // Connection accepted
                status = Status::Done;
            }
            else
            {
                // Connection refused
                status = priv::SocketImpl::getErrorStatus();
            }
        }
        else
        {
            // Failed to connect before timeout is over
            status = priv::SocketImpl::getErrorStatus();
        }
    }

    // Switch back to blocking mode
    setBlocking(true);

    return status;
}


////////////////////////////////////////////////////////////
void TcpSocket::disconnect()
{
    if (m_impl->tlsState)
    {
        if (m_impl->tlsState->handshakeComplete)
        {
            if (auto result = mbedtls_ssl_close_notify(&m_impl->tlsState->sslContext);
                (result != 0 && result != MBEDTLS_ERR_SSL_WANT_READ && result != MBEDTLS_ERR_SSL_WANT_WRITE &&
                 result != MBEDTLS_ERR_NET_CONN_RESET))
                err() << "Failed to notify TLS peer connection is being closed: " << tlsErrorString(result) << std::endl;
        }

        m_impl->tlsState.reset();
    }

    // Close the socket
    close();

    // Reset the pending packet data
    m_pendingPacket = PendingPacket();
}


////////////////////////////////////////////////////////////
TcpSocket::TlsStatus TcpSocket::setupTlsClient(const String& hostname, bool verifyPeer)
{
    return m_impl->setupTls(*this, hostname, verifyPeer, nullptr, 0, nullptr, 0, nullptr, 0);
}


////////////////////////////////////////////////////////////
TcpSocket::TlsStatus TcpSocket::setupTlsClient(const sf::String& hostname, const char* certificateChainData)
{
    // Mbed TLS expects the terminating NULL to be part of the PEM data
    // When we pass the data to MbedTLS have to add 1 to the size of the strings to include the NULL-terminator
    return setupTlsClient(hostname,
                          reinterpret_cast<const std::byte*>(certificateChainData),
                          std::strlen(certificateChainData) + 1);
}


////////////////////////////////////////////////////////////
TcpSocket::TlsStatus TcpSocket::setupTlsClient(const String&    hostname,
                                               const std::byte* certificateChainData,
                                               std::size_t      certificateChainSize)
{
    assert(certificateChainData != nullptr && certificateChainSize > 0 && "Certificate chain data not valid");

    if (certificateChainData == nullptr || certificateChainSize == 0)
        return TlsStatus::Error;

    return m_impl->setupTls(*this, hostname, true, certificateChainData, certificateChainSize, nullptr, 0, nullptr, 0);
}


////////////////////////////////////////////////////////////
TcpSocket::TlsStatus TcpSocket::setupTlsClient(const sf::String& hostname, std::string_view certificateChainData)
{
    // Mbed TLS expects the terminating NULL to be part of the PEM data
    // For this reason we have to make a std::string copy of the function arguments
    // since std::string_view does not guarantee NULL-terminated data
    // When we pass the data to MbedTLS have to add 1 to the size of the strings to include the NULL-terminator
    const std::string certificateChainDataCopy(certificateChainData);

    return setupTlsClient(hostname,
                          reinterpret_cast<const std::byte*>(certificateChainDataCopy.c_str()),
                          certificateChainDataCopy.size() + 1);
}


////////////////////////////////////////////////////////////
TcpSocket::TlsStatus TcpSocket::setupTlsServer(
    const std::byte* certificateChainData,
    std::size_t      certificateChainSize,
    const std::byte* privateKeyData,
    std::size_t      privateKeySize,
    const std::byte* privateKeyPasswordData,
    std::size_t      privateKeyPasswordSize)
{
    assert(certificateChainData != nullptr && certificateChainSize > 0 && "Certificate chain data not valid");
    assert(privateKeyData != nullptr && privateKeySize > 0 && "Private key data not valid");

    if (certificateChainData == nullptr || certificateChainSize == 0 || privateKeyData == nullptr || privateKeySize == 0)
        return TlsStatus::Error;

    return m_impl->setupTls(*this,
                            {},
                            false,
                            certificateChainData,
                            certificateChainSize,
                            privateKeyData,
                            privateKeySize,
                            privateKeyPasswordData,
                            privateKeyPasswordSize);
}


////////////////////////////////////////////////////////////
TcpSocket::TlsStatus TcpSocket::setupTlsServer(std::string_view certificateChainData,
                                               std::string_view privateKeyData,
                                               std::string_view privateKeyPasswordData)
{
    // Mbed TLS expects the terminating NULL to be part of the PEM data
    // For this reason we have to make a std::string copy of the function arguments
    // since std::string_view does not guarantee NULL-terminated data
    // When we pass the data to MbedTLS have to add 1 to the size of the strings to include the NULL-terminator
    const std::string certificateChainDataCopy(certificateChainData);
    const std::string privateKeyDataCopy(privateKeyData);
    const std::string privateKeyPasswordDataCopy(privateKeyPasswordData);

    return setupTlsServer(reinterpret_cast<const std::byte*>(certificateChainDataCopy.c_str()),
                          certificateChainDataCopy.size() + 1,
                          reinterpret_cast<const std::byte*>(privateKeyDataCopy.c_str()),
                          privateKeyDataCopy.size() + 1,
                          reinterpret_cast<const std::byte*>(privateKeyPasswordDataCopy.c_str()),
                          privateKeyPasswordDataCopy.size() + 1);
}


////////////////////////////////////////////////////////////
std::optional<std::string> TcpSocket::getCurrentCiphersuiteName() const
{
    if (!m_impl->tlsState)
        return std::nullopt;

    if (const auto* cipersuiteName = mbedtls_ssl_get_ciphersuite(&m_impl->tlsState->sslContext); cipersuiteName)
        return cipersuiteName;

    return std::nullopt;
}


////////////////////////////////////////////////////////////
Socket::Status TcpSocket::send(const void* data, std::size_t size)
{
    if (!isBlocking())
        err() << "Warning: Partial sends might not be handled properly." << std::endl;

    std::size_t sent = 0;

    return send(data, size, sent);
}


////////////////////////////////////////////////////////////
Socket::Status TcpSocket::send(const void* data, std::size_t size, std::size_t& sent)
{
    // Check the parameters
    if (!data || (size == 0))
    {
        err() << "Cannot send data over the network (no data to send)" << std::endl;
        return Status::Error;
    }

    // Loop until every byte has been sent
    int result = 0;
    for (sent = 0; sent < size; sent += static_cast<std::size_t>(result))
    {
        if (m_impl->tlsState)
        {
            // Handle sending over a TLS stream
            assert(m_impl->tlsState->handshakeComplete &&
                   "TLS handshake must be complete before receiving application data");

            if (!m_impl->tlsState->handshakeComplete)
            {
                err() << "TLS handshake must be complete before receiving application data" << std::endl;
                return Status::Error;
            }

            result = mbedtls_ssl_write(&m_impl->tlsState->sslContext,
                                       static_cast<const unsigned char*>(data) + sent,
                                       size - sent);

            switch (result)
            {
                case MBEDTLS_ERR_SSL_WANT_READ:
                    [[fallthrough]];
                case MBEDTLS_ERR_SSL_WANT_WRITE:
                    [[fallthrough]];
                case MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS:
                    [[fallthrough]];
#if defined(MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET)
                case MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET:
                    [[fallthrough]];
#endif
                case MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS:
                    return Status::Partial;
                case MBEDTLS_ERR_NET_CONN_RESET:
                    sent = 0;
                    return Status::Disconnected;
                default:
                    break;
            }

            // If an any other error occurred, reset the TLS state
            if (result < 0)
            {
                m_impl->tlsState.reset();
                return Status::Error;
            }
        }
        else
        {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wuseless-cast"
            // Send a chunk of data
            result = static_cast<int>(::send(getNativeHandle(),
                                             static_cast<const char*>(data) + sent,
                                             static_cast<priv::SocketImpl::Size>(size - sent),
                                             flags));
#pragma GCC diagnostic pop
        }

        // Check for errors
        if (result < 0)
        {
            const Status status = priv::SocketImpl::getErrorStatus();

            if ((status == Status::NotReady) && sent)
                return Status::Partial;

            return status;
        }
    }

    return Status::Done;
}


////////////////////////////////////////////////////////////
Socket::Status TcpSocket::receive(void* data, std::size_t size, std::size_t& received)
{
    // First clear the variables to fill
    received = 0;

    // Check the destination buffer
    if (!data)
    {
        err() << "Cannot receive data from the network (the destination buffer is invalid)" << std::endl;
        return Status::Error;
    }

    auto sizeReceived = 0;

    if (m_impl->tlsState)
    {
        // Handle receiving over a TLS stream
        assert(m_impl->tlsState->handshakeComplete && "TLS handshake must be complete before sending application data");

        if (!m_impl->tlsState->handshakeComplete)
        {
            err() << "TLS handshake must be complete before sending application data" << std::endl;
            return Status::Error;
        }

        sizeReceived = mbedtls_ssl_read(&m_impl->tlsState->sslContext, static_cast<unsigned char*>(data), size);

        switch (sizeReceived)
        {
            case MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY:
                [[fallthrough]];
            case MBEDTLS_ERR_NET_CONN_RESET:
                received = 0;
                return Status::Disconnected;
            case MBEDTLS_ERR_SSL_WANT_READ:
                [[fallthrough]];
            case MBEDTLS_ERR_SSL_WANT_WRITE:
                [[fallthrough]];
            case MBEDTLS_ERR_SSL_ASYNC_IN_PROGRESS:
                [[fallthrough]];
#if defined(MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET)
            case MBEDTLS_ERR_SSL_RECEIVED_NEW_SESSION_TICKET:
                [[fallthrough]];
#endif
            case MBEDTLS_ERR_SSL_CRYPTO_IN_PROGRESS:
                received = 0;
                return Status::Partial;
            default:
                break;
        }

        // If any other error occurred or the TLS stream has no more data to provide, reset the TLS state
        if (sizeReceived < 0)
        {
            m_impl->tlsState.reset();
            return Status::Error;
        }

        if (sizeReceived == 0)
            m_impl->tlsState.reset();
    }
    else
    {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wuseless-cast"
        // Receive a chunk of bytes
        sizeReceived = static_cast<int>(
            recv(getNativeHandle(), static_cast<char*>(data), static_cast<priv::SocketImpl::Size>(size), flags));
#pragma GCC diagnostic pop
    }

    // Check the number of bytes received
    if (sizeReceived > 0)
    {
        received = static_cast<std::size_t>(sizeReceived);
        return Status::Done;
    }
    if (sizeReceived == 0)
    {
        return Status::Disconnected;
    }

    return priv::SocketImpl::getErrorStatus();
}


////////////////////////////////////////////////////////////
Socket::Status TcpSocket::send(Packet& packet)
{
    // TCP is a stream protocol, it doesn't preserve messages boundaries.
    // This means that we have to send the packet size first, so that the
    // receiver knows the actual end of the packet in the data stream.

    // We allocate an extra memory block so that the size can be sent
    // together with the data in a single call. This may seem inefficient,
    // but it is actually required to avoid partial send, which could cause
    // data corruption on the receiving end.

    // Get the data to send from the packet
    std::size_t size = 0;
    const void* data = packet.onSend(size);

    // First convert the packet size to network byte order
    std::uint32_t packetSize = htonl(static_cast<std::uint32_t>(size));

    // Allocate memory for the data block to send
    m_blockToSendBuffer.resize(sizeof(packetSize) + size);

// Copy the packet size and data into the block to send
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnull-dereference" // False positive.
    std::memcpy(m_blockToSendBuffer.data(), &packetSize, sizeof(packetSize));
#pragma GCC diagnostic pop
    if (size > 0)
        std::memcpy(m_blockToSendBuffer.data() + sizeof(packetSize), data, size);

// These warnings are ignored here for portability, as even on Windows the
// signature of `send` might change depending on whether Win32 or MinGW is
// being used.
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wuseless-cast"
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wsign-conversion"
    // Send the data block
    std::size_t  sent   = 0;
    const Status status = send(m_blockToSendBuffer.data() + packet.m_sendPos,
                               static_cast<priv::SocketImpl::Size>(m_blockToSendBuffer.size() - packet.m_sendPos),
                               sent);
#pragma GCC diagnostic pop
#pragma GCC diagnostic pop

    // In the case of a partial send, record the location to resume from
    if (status == Status::Partial)
    {
        packet.m_sendPos += sent;
    }
    else if (status == Status::Done)
    {
        packet.m_sendPos = 0;
    }

    return status;
}


////////////////////////////////////////////////////////////
Socket::Status TcpSocket::receive(Packet& packet)
{
    // First clear the variables to fill
    packet.clear();

    // We start by getting the size of the incoming packet
    std::uint32_t packetSize = 0;
    std::size_t   received   = 0;
    if (m_pendingPacket.sizeReceived < sizeof(m_pendingPacket.size))
    {
        // Loop until we've received the entire size of the packet
        // (even a 4 byte variable may be received in more than one call)
        while (m_pendingPacket.sizeReceived < sizeof(m_pendingPacket.size))
        {
            char*        data   = reinterpret_cast<char*>(&m_pendingPacket.size) + m_pendingPacket.sizeReceived;
            const Status status = receive(data, sizeof(m_pendingPacket.size) - m_pendingPacket.sizeReceived, received);
            m_pendingPacket.sizeReceived += received;

            if (status != Status::Done)
                return status;
        }

        // The packet size has been fully received
        packetSize = ntohl(m_pendingPacket.size);
    }
    else
    {
        // The packet size has already been received in a previous call
        packetSize = ntohl(m_pendingPacket.size);
    }

    // Loop until we receive all the packet data
    std::array<char, 1024> buffer{};
    while (m_pendingPacket.data.size() < packetSize)
    {
        // Receive a chunk of data
        const std::size_t sizeToGet = std::min(packetSize - m_pendingPacket.data.size(), buffer.size());
        const Status      status    = receive(buffer.data(), sizeToGet, received);
        if (status != Status::Done)
            return status;

        // Append it into the packet
        if (received > 0)
        {
            m_pendingPacket.data.resize(m_pendingPacket.data.size() + received);
            std::byte* begin = m_pendingPacket.data.data() + m_pendingPacket.data.size() - received;
            std::memcpy(begin, buffer.data(), received);
        }
    }

    // We have received all the packet data: we can copy it to the user packet
    if (!m_pendingPacket.data.empty())
        packet.onReceive(m_pendingPacket.data.data(), m_pendingPacket.data.size());

    // Clear the pending packet data
    m_pendingPacket = PendingPacket();

    return Status::Done;
}

} // namespace sf
