/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
 * This file is part of the LibreOffice project.
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

#ifndef INCLUDED_LOOLPROTOCOL_HPP
#define INCLUDED_LOOLPROTOCOL_HPP

#include <cstdint>
#include <cstring>
#include <map>
#include <sstream>
#include <string>

#include <Poco/Format.h>
#include <Poco/StringTokenizer.h>

#include <Poco/Net/WebSocket.h>

#define LOK_USE_UNSTABLE_API
#include <LibreOfficeKit/LibreOfficeKitEnums.h>

namespace LOOLProtocol
{
    // Protocol Version Number.
    // See protocol.txt.
    constexpr unsigned ProtocolMajorVersionNumber = 0;
    constexpr unsigned ProtocolMinorVersionNumber = 1;

    inline std::string GetProtocolVersion()
    {
        return std::to_string(ProtocolMajorVersionNumber) + '.'
            + std::to_string(ProtocolMinorVersionNumber);
    }

    // Parse a string into a version tuple.
    // Negative numbers for error.
    std::tuple<int, int, std::string> ParseVersion(const std::string& version);

    bool stringToInteger(const std::string& input, int& value);
    bool stringToUInt64(const std::string& input, uint64_t& value);

    inline
    bool parseNameValuePair(const std::string& token, std::string& name, std::string& value, const char delim = '=')
    {
        const auto mid = token.find_first_of(delim);
        if (mid != std::string::npos)
        {
            name = token.substr(0, mid);
            value = token.substr(mid + 1);
            return true;
        }

        return false;
    }

    inline
    bool parseNameIntegerPair(const std::string& token, std::string& name, int& value)
    {
        std::string strValue;
        return parseNameValuePair(token, name, strValue, '=') && stringToInteger(strValue, value);
    }

    bool getTokenInteger(const std::string& token, const std::string& name, int& value);
    bool getTokenUInt64(const std::string& token, const std::string& name, uint64_t& value);
    bool getTokenString(const std::string& token, const std::string& name, std::string& value);
    bool getTokenKeyword(const std::string& token, const std::string& name, const std::map<std::string, int>& map, int& value);

    bool getTokenInteger(const Poco::StringTokenizer& tokens, const std::string& name, int& value);
    bool getTokenString(const Poco::StringTokenizer& tokens, const std::string& name, std::string& value);
    bool getTokenKeyword(const Poco::StringTokenizer& tokens, const std::string& name, const std::map<std::string, int>& map, int& value);

    bool getTokenInteger(const std::vector<std::string>& tokens, const std::string& name, int& value);

    inline bool getTokenString(const std::vector<std::string>& tokens,
                               const std::string& name,
                               std::string& value)
    {
        for (const auto& token : tokens)
        {
            if (getTokenString(token, name, value))
            {
                return true;
            }
        }

        return false;
    }

    bool getTokenStringFromMessage(const std::string& message, const std::string& name, std::string& value);
    bool getTokenKeywordFromMessage(const std::string& message, const std::string& name, const std::map<std::string, int>& map, int& value);

    /// Tokenize space-delimited values until we hit new-line or the end.
    inline
    std::vector<std::string> tokenize(const char* data, const size_t size, const char delimeter = ' ')
    {
        std::vector<std::string> tokens;
        if (size == 0 || data == nullptr)
        {
            return tokens;
        }

        const char* start = data;
        const char* end = data;
        for (size_t i = 0; i < size && data[i] != '\n'; ++i, ++end)
        {
            if (data[i] == delimeter)
            {
                if (start != end && *start != delimeter)
                {
                    tokens.emplace_back(start, end);
                }

                start = end;
            }
            else if (*start == delimeter)
            {
                ++start;
            }
        }

        if (start != end && *start != delimeter && *start != '\n')
        {
            tokens.emplace_back(start, end);
        }

        return tokens;
    }

    inline
    std::vector<std::string> tokenize(const std::string& s, const char delimeter = ' ')
    {
        return tokenize(s.data(), s.size(), delimeter);
    }

    inline bool getTokenIntegerFromMessage(const std::string& message, const std::string& name, int& value)
    {
        return getTokenInteger(tokenize(message), name, value);
    }

    inline size_t getDelimiterPosition(const char* message, const int length, const char delim)
    {
        if (message && length > 0)
        {
            const char *founddelim = static_cast<const char *>(std::memchr(message, delim, length));
            const auto size = (founddelim == nullptr ? length : founddelim - message);
            return size;
        }

        return 0;
    }

    inline
    std::string getDelimitedInitialSubstring(const char *message, const int length, const char delim)
    {
        const auto size = getDelimiterPosition(message, length, delim);
        return std::string(message, size);
    }

    /// Returns the first token of a message.
    inline
    std::string getFirstToken(const char *message, const int length, const char delim = ' ')
    {
        return getDelimitedInitialSubstring(message, length, delim);
    }

    template <typename T>
    std::string getFirstToken(const T& message, const char delim = ' ')
    {
        return getFirstToken(message.data(), message.size(), delim);
    }

    inline
    bool matchPrefix(const std::string& prefix, const std::string& message)
    {
        return (message.size() >= prefix.size() &&
                message.compare(0, prefix.size(), prefix) == 0);
    }

    inline
    bool matchPrefix(const std::string& prefix, const std::vector<char>& message)
    {
        return (message.size() >= prefix.size() &&
                prefix.compare(0, prefix.size(), message.data(), prefix.size()) == 0);
    }

    inline
    bool matchPrefix(const std::string& prefix, const std::string& message, const bool ignoreWhitespace)
    {
        if (ignoreWhitespace)
        {
            const auto posPre = prefix.find_first_not_of(' ');
            const auto posMsg = message.find_first_not_of(' ');

            return matchPrefix(posPre == std::string::npos ? prefix : prefix.substr(posPre),
                               posMsg == std::string::npos ? message : message.substr(posMsg));
        }
        else
        {
            return matchPrefix(prefix, message);
        }
    }

    /// Returns true if the token is a user-interaction token.
    /// Currently this excludes commands sent automatically.
    /// Notice that this doesn't guarantee editing activity,
    /// rather just user interaction with the UI.
    inline
    bool tokenIndicatesUserInteraction(const std::string& token)
    {
        // Exclude tokens that include these keywords, such as canceltiles statusindicator.

        // FIXME: This is wrong. That the token happens to contain (or not) a certain substring is
        // no guarantee that it "indicates user interaction". It might be like that at the moment,
        // but that is coincidental. We should check what the actual whole token is, at least, not
        // look for a substring.

        return (token.find("tile") == std::string::npos &&
                token.find("status") == std::string::npos &&
                token.find("state") == std::string::npos &&
                token != "userinactive");
    }

    /// Returns the first line of a message.
    inline
    std::string getFirstLine(const char *message, const int length)
    {
        return getDelimitedInitialSubstring(message, length, '\n');
    }

    /// Returns the first line of any data which payload char*.
    template <typename T>
    std::string getFirstLine(const T& message)
    {
        return getFirstLine(message.data(), message.size());
    }

    /// Returns an abbereviation of the message (the first line, indicating truncation). We assume
    /// that it adhers to the LOOL protocol, i.e. that there is always a first (or only) line that
    /// is in printable UTF-8. I.e. no encoding of binary bytes is done. The format of the result is
    /// not guaranteed to be stable. It is to be used for logging purposes only, not for decoding
    /// protocol frames.
    inline
    std::string getAbbreviatedMessage(const char *message, const int length)
    {
        if (message == nullptr || length <= 0)
        {
            return std::string();
        }

        const auto firstLine = getFirstLine(message, std::min(length, 500));

        // If first line is less than the length (minus newline), add ellipsis.
        if (firstLine.size() < static_cast<std::string::size_type>(length) - 1)
        {
            return firstLine + "...";
        }

        return firstLine;
    }

    inline std::string getAbbreviatedMessage(const std::string& message)
    {
        const auto pos = getDelimiterPosition(message.data(), std::min(message.size(), 500UL), '\n');

        // If first line is less than the length (minus newline), add ellipsis.
        if (pos < static_cast<std::string::size_type>(message.size()) - 1)
        {
            return message.substr(0, pos) + "...";
        }

        return message;
    }

    template <typename T>
    std::string getAbbreviatedMessage(const T& message)
    {
        return getAbbreviatedMessage(message.data(), message.size());
    }

    // Return a string dump of a WebSocket frame: Its opcode, length, first line (if present),
    // flags. For human-readable logging purposes. Format not guaranteed to be stable. Not to be
    // inspected programmatically.
    inline
    std::string getAbbreviatedFrameDump(const char *message, const int length, const int flags)
    {
        std::ostringstream result;
        switch (flags & Poco::Net::WebSocket::FRAME_OP_BITMASK)
        {
#define CASE(x) case Poco::Net::WebSocket::FRAME_OP_##x: result << #x; break
        CASE(CONT);
        CASE(TEXT);
        CASE(BINARY);
        CASE(CLOSE);
        CASE(PING);
        CASE(PONG);
#undef CASE
        default:
            result << Poco::format("%#x", flags);
            break;
        }
        result << " " << length << " bytes";

        if (length > 0 &&
            ((flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) == Poco::Net::WebSocket::FRAME_OP_TEXT ||
             (flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) == Poco::Net::WebSocket::FRAME_OP_BINARY ||
             (flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) == Poco::Net::WebSocket::FRAME_OP_PING ||
             (flags & Poco::Net::WebSocket::FRAME_OP_BITMASK) == Poco::Net::WebSocket::FRAME_OP_PONG))
            result << ": '" << getAbbreviatedMessage(message, length) << "'";
        return result.str();
    }
};

#endif

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
