/*
 *  IXWebSocketPerMessageDeflateOptions.cpp
 *  Author: Benjamin Sergeant
 *  Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
 */

#include "IXWebSocketPerMessageDeflateOptions.h"

#include <algorithm>
#include <cctype>
#include <sstream>

namespace ix
{
    /// Default values as defined in the RFC
    const uint8_t WebSocketPerMessageDeflateOptions::kDefaultServerMaxWindowBits = 15;
    const uint8_t WebSocketPerMessageDeflateOptions::kDefaultClientMaxWindowBits = 15;
#ifdef IXWEBSOCKET_USE_ZLIB
    static const uint8_t minServerMaxWindowBits = 8;
    static const uint8_t maxServerMaxWindowBits = 15;
    static const uint8_t minClientMaxWindowBits = 8;
    static const uint8_t maxClientMaxWindowBits = 15;
#endif

    WebSocketPerMessageDeflateOptions::WebSocketPerMessageDeflateOptions(
        bool enabled,
        bool clientNoContextTakeover,
        bool serverNoContextTakeover,
        uint8_t clientMaxWindowBits,
        uint8_t serverMaxWindowBits,
        bool sendClientServerMaxWindowBits)
    {
        _enabled = enabled;
        _clientNoContextTakeover = clientNoContextTakeover;
        _serverNoContextTakeover = serverNoContextTakeover;
        _clientMaxWindowBits = clientMaxWindowBits;
        _serverMaxWindowBits = serverMaxWindowBits;
        _sendClientServerMaxWindowBits = sendClientServerMaxWindowBits;

        sanitizeClientMaxWindowBits();
    }

    //
    // Four extension parameters are defined for "permessage-deflate" to
    // help endpoints manage per-connection resource usage.
    //
    // - "server_no_context_takeover"
    // - "client_no_context_takeover"
    // - "server_max_window_bits"
    // - "client_max_window_bits"
    //
    // Server response could look like that:
    //
    // Sec-WebSocket-Extensions: permessage-deflate; client_no_context_takeover;
    // server_no_context_takeover
    //
    WebSocketPerMessageDeflateOptions::WebSocketPerMessageDeflateOptions(std::string extension)
    {
        extension = removeSpaces(extension);

        _enabled = false;
        _clientNoContextTakeover = false;
        _serverNoContextTakeover = false;
        _sendClientServerMaxWindowBits = true;
        _clientMaxWindowBits = kDefaultClientMaxWindowBits;
        _serverMaxWindowBits = kDefaultServerMaxWindowBits;

#ifdef IXWEBSOCKET_USE_ZLIB
        // Split by ;
        std::string token;
        std::stringstream tokenStream(extension);

        while (std::getline(tokenStream, token, ';'))
        {
            if (token == "permessage-deflate")
            {
                _enabled = true;
            }

            if (token == "server_no_context_takeover")
            {
                _serverNoContextTakeover = true;
            }

            if (token == "client_no_context_takeover")
            {
                _clientNoContextTakeover = true;
            }

            if (startsWith(token, "server_max_window_bits="))
            {
                uint8_t x = strtol(token.substr(token.find_last_of("=") + 1).c_str(), nullptr, 10);

                // Sanitize values to be in the proper range [8, 15] in
                // case a server would give us bogus values
                _serverMaxWindowBits =
                    std::min(maxServerMaxWindowBits, std::max(x, minServerMaxWindowBits));
            }

            if (startsWith(token, "client_max_window_bits="))
            {
                uint8_t x = strtol(token.substr(token.find_last_of("=") + 1).c_str(), nullptr, 10);

                // Sanitize values to be in the proper range [8, 15] in
                // case a server would give us bogus values
                _clientMaxWindowBits =
                    std::min(maxClientMaxWindowBits, std::max(x, minClientMaxWindowBits));

                sanitizeClientMaxWindowBits();
            }
        }
#endif
    }

    void WebSocketPerMessageDeflateOptions::sanitizeClientMaxWindowBits()
    {
        // zlib/deflate has a bug with windowsbits == 8, so we silently upgrade it to 9
        // See https://bugs.chromium.org/p/chromium/issues/detail?id=691074
        if (_clientMaxWindowBits == 8)
        {
            _clientMaxWindowBits = 9;
        }
    }

    std::string WebSocketPerMessageDeflateOptions::generateHeader()
    {
#ifdef IXWEBSOCKET_USE_ZLIB
        std::stringstream ss;
        ss << "Sec-WebSocket-Extensions: permessage-deflate";

        if (_clientNoContextTakeover) ss << "; client_no_context_takeover";
        if (_serverNoContextTakeover) ss << "; server_no_context_takeover";

        if (_sendClientServerMaxWindowBits)
        {
            ss << "; server_max_window_bits=" << static_cast<int>(_serverMaxWindowBits);
            ss << "; client_max_window_bits=" << static_cast<int>(_clientMaxWindowBits);
        }

        ss << "\r\n";

        return ss.str();
#else
        return std::string();
#endif
    }

    bool WebSocketPerMessageDeflateOptions::enabled() const
    {
#ifdef IXWEBSOCKET_USE_ZLIB
        return _enabled;
#else
        return false;
#endif
    }

    bool WebSocketPerMessageDeflateOptions::getClientNoContextTakeover() const
    {
        return _clientNoContextTakeover;
    }

    bool WebSocketPerMessageDeflateOptions::getServerNoContextTakeover() const
    {
        return _serverNoContextTakeover;
    }

    uint8_t WebSocketPerMessageDeflateOptions::getClientMaxWindowBits() const
    {
        return _clientMaxWindowBits;
    }

    uint8_t WebSocketPerMessageDeflateOptions::getServerMaxWindowBits() const
    {
        return _serverMaxWindowBits;
    }

    bool WebSocketPerMessageDeflateOptions::getSendClientServerMaxWindowBits() const
    {
        return _sendClientServerMaxWindowBits;
    }

    bool WebSocketPerMessageDeflateOptions::startsWith(const std::string& str,
                                                       const std::string& start)
    {
        return str.compare(0, start.length(), start) == 0;
    }

    std::string WebSocketPerMessageDeflateOptions::removeSpaces(const std::string& str)
    {
        std::string out(str);
        out.erase(
            std::remove_if(out.begin(), out.end(), [](unsigned char x) { return std::isspace(x); }),
            out.end());

        return out;
    }
} // namespace ix
