// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright OpenBMC Authors
#include "json_html_serializer.hpp"

#include "http_response.hpp"

#include <boost/beast/http/field.hpp>
#include <nlohmann/json.hpp>

#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <iterator>
#include <string>
#include <type_traits>
#include <utility>

namespace json_html_util
{

static constexpr uint8_t utf8Accept = 0;
static constexpr uint8_t utf8Reject = 1;

static uint8_t decode(uint8_t& state, uint32_t& codePoint,
                      const uint8_t byte) noexcept
{
    // clang-format off
    static const std::array<std::uint8_t, 400> utf8d =
    {
        {
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 00..1F
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 20..3F
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 40..5F
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 60..7F
            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, // 80..9F
            7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, // A0..BF
            8, 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // C0..DF
            0xA, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x3, 0x4, 0x3, 0x3, // E0..EF
            0xB, 0x6, 0x6, 0x6, 0x5, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, 0x8, // F0..FF
            0x0, 0x1, 0x2, 0x3, 0x5, 0x8, 0x7, 0x1, 0x1, 0x1, 0x4, 0x6, 0x1, 0x1, 0x1, 0x1, // s0..s0
            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, // s1..s2
            1, 2, 1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, // s3..s4
            1, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, // s5..s6
            1, 3, 1, 1, 1, 1, 1, 3, 1, 3, 1, 1, 1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // s7..s8
        }
    };
    // clang-format on

    if (state > 0x8)
    {
        return state;
    }

    const uint8_t type = utf8d[byte];

    codePoint = (state != utf8Accept)
                    ? (byte & 0x3fU) | (codePoint << 6)
                    : static_cast<uint32_t>(0xff >> type) & (byte);

    state = utf8d[256U + state * 16U + type];
    return state;
}

static void dumpEscaped(std::string& out, const std::string& str)
{
    std::array<char, 512> stringBuffer{{}};
    uint32_t codePoint = 0;
    uint8_t state = utf8Accept;
    std::size_t bytes = 0; // number of bytes written to string_buffer

    // number of bytes written at the point of the last valid byte
    std::size_t bytesAfterLastAccept = 0;
    std::size_t undumpedChars = 0;

    for (std::size_t i = 0; i < str.size(); ++i)
    {
        const uint8_t byte = static_cast<uint8_t>(str[i]);

        switch (decode(state, codePoint, byte))
        {
            case utf8Accept: // decode found a new code point
            {
                switch (codePoint)
                {
                    case 0x08: // backspace
                    {
                        stringBuffer[bytes++] = '\\';
                        stringBuffer[bytes++] = 'b';
                        break;
                    }

                    case 0x09: // horizontal tab
                    {
                        stringBuffer[bytes++] = '\\';
                        stringBuffer[bytes++] = 't';
                        break;
                    }

                    case 0x0A: // newline
                    {
                        stringBuffer[bytes++] = '\\';
                        stringBuffer[bytes++] = 'n';
                        break;
                    }

                    case 0x0C: // formfeed
                    {
                        stringBuffer[bytes++] = '\\';
                        stringBuffer[bytes++] = 'f';
                        break;
                    }

                    case 0x0D: // carriage return
                    {
                        stringBuffer[bytes++] = '\\';
                        stringBuffer[bytes++] = 'r';
                        break;
                    }

                    case 0x22: // quotation mark
                    {
                        stringBuffer[bytes++] = '&';
                        stringBuffer[bytes++] = 'q';
                        stringBuffer[bytes++] = 'u';
                        stringBuffer[bytes++] = 'o';
                        stringBuffer[bytes++] = 't';
                        stringBuffer[bytes++] = ';';
                        break;
                    }

                    case 0x27: // apostrophe
                    {
                        stringBuffer[bytes++] = '&';
                        stringBuffer[bytes++] = 'a';
                        stringBuffer[bytes++] = 'p';
                        stringBuffer[bytes++] = 'o';
                        stringBuffer[bytes++] = 's';
                        stringBuffer[bytes++] = ';';
                        break;
                    }

                    case 0x26: // ampersand
                    {
                        stringBuffer[bytes++] = '&';
                        stringBuffer[bytes++] = 'a';
                        stringBuffer[bytes++] = 'm';
                        stringBuffer[bytes++] = 'p';
                        stringBuffer[bytes++] = ';';
                        break;
                    }

                    case 0x3C: // less than
                    {
                        stringBuffer[bytes++] = '\\';
                        stringBuffer[bytes++] = 'l';
                        stringBuffer[bytes++] = 't';
                        stringBuffer[bytes++] = ';';
                        break;
                    }

                    case 0x3E: // greater than
                    {
                        stringBuffer[bytes++] = '\\';
                        stringBuffer[bytes++] = 'g';
                        stringBuffer[bytes++] = 't';
                        stringBuffer[bytes++] = ';';
                        break;
                    }

                    default:
                    {
                        // escape control characters (0x00..0x1F)
                        if ((codePoint <= 0x1F) or (codePoint >= 0x7F))
                        {
                            if (codePoint <= 0xFFFF)
                            {
                                // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
                                std::snprintf(&stringBuffer[bytes], 7,
                                              "\\u%04x",
                                              static_cast<uint16_t>(codePoint));
                                bytes += 6;
                            }
                            else
                            {
                                // NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
                                std::snprintf(
                                    &stringBuffer[bytes], 13, "\\u%04x\\u%04x",
                                    static_cast<uint16_t>(
                                        0xD7C0 + (codePoint >> 10)),
                                    static_cast<uint16_t>(
                                        0xDC00 + (codePoint & 0x3FF)));
                                bytes += 12;
                            }
                        }
                        else
                        {
                            // copy byte to buffer (all previous bytes
                            // been copied have in default case above)
                            stringBuffer[bytes++] = str[i];
                        }
                        break;
                    }
                }

                // write buffer and reset index; there must be 13 bytes
                // left, as this is the maximal number of bytes to be
                // written ("\uxxxx\uxxxx\0") for one code point
                if (stringBuffer.size() - bytes < 13)
                {
                    out.append(stringBuffer.data(), bytes);
                    bytes = 0;
                }

                // remember the byte position of this accept
                bytesAfterLastAccept = bytes;
                undumpedChars = 0;
                break;
            }

            case utf8Reject: // decode found invalid UTF-8 byte
            {
                // in case we saw this character the first time, we
                // would like to read it again, because the byte
                // may be OK for itself, but just not OK for the
                // previous sequence
                if (undumpedChars > 0)
                {
                    --i;
                }

                // reset length buffer to the last accepted index;
                // thus removing/ignoring the invalid characters
                bytes = bytesAfterLastAccept;

                stringBuffer[bytes++] = '\\';
                stringBuffer[bytes++] = 'u';
                stringBuffer[bytes++] = 'f';
                stringBuffer[bytes++] = 'f';
                stringBuffer[bytes++] = 'f';
                stringBuffer[bytes++] = 'd';

                bytesAfterLastAccept = bytes;

                undumpedChars = 0;

                // continue processing the string
                state = utf8Accept;
                break;
            }

            default: // decode found yet incomplete multi-byte code point
            {
                ++undumpedChars;
                break;
            }
        }
    }

    // we finished processing the string
    if (state == utf8Accept)
    {
        // write buffer
        if (bytes > 0)
        {
            out.append(stringBuffer.data(), bytes);
        }
    }
    else
    {
        // write all accepted bytes
        out.append(stringBuffer.data(), bytesAfterLastAccept);
        out += "\\ufffd";
    }
}

static unsigned int countDigits(uint64_t number) noexcept
{
    unsigned int nDigits = 1;
    for (;;)
    {
        if (number < 10)
        {
            return nDigits;
        }
        if (number < 100)
        {
            return nDigits + 1;
        }
        if (number < 1000)
        {
            return nDigits + 2;
        }
        if (number < 10000)
        {
            return nDigits + 3;
        }
        number = number / 10000U;
        nDigits += 4;
    }
}

template <typename NumberType,
          std::enable_if_t<std::is_same<NumberType, uint64_t>::value or
                               std::is_same<NumberType, int64_t>::value,
                           int> = 0>
void dumpInteger(std::string& out, NumberType number)
{
    std::array<char, 64> numberbuffer{{}};

    static constexpr std::array<std::array<char, 2>, 100> digitsTo99{{
        {'0', '0'}, {'0', '1'}, {'0', '2'}, {'0', '3'}, {'0', '4'}, {'0', '5'},
        {'0', '6'}, {'0', '7'}, {'0', '8'}, {'0', '9'}, {'1', '0'}, {'1', '1'},
        {'1', '2'}, {'1', '3'}, {'1', '4'}, {'1', '5'}, {'1', '6'}, {'1', '7'},
        {'1', '8'}, {'1', '9'}, {'2', '0'}, {'2', '1'}, {'2', '2'}, {'2', '3'},
        {'2', '4'}, {'2', '5'}, {'2', '6'}, {'2', '7'}, {'2', '8'}, {'2', '9'},
        {'3', '0'}, {'3', '1'}, {'3', '2'}, {'3', '3'}, {'3', '4'}, {'3', '5'},
        {'3', '6'}, {'3', '7'}, {'3', '8'}, {'3', '9'}, {'4', '0'}, {'4', '1'},
        {'4', '2'}, {'4', '3'}, {'4', '4'}, {'4', '5'}, {'4', '6'}, {'4', '7'},
        {'4', '8'}, {'4', '9'}, {'5', '0'}, {'5', '1'}, {'5', '2'}, {'5', '3'},
        {'5', '4'}, {'5', '5'}, {'5', '6'}, {'5', '7'}, {'5', '8'}, {'5', '9'},
        {'6', '0'}, {'6', '1'}, {'6', '2'}, {'6', '3'}, {'6', '4'}, {'6', '5'},
        {'6', '6'}, {'6', '7'}, {'6', '8'}, {'6', '9'}, {'7', '0'}, {'7', '1'},
        {'7', '2'}, {'7', '3'}, {'7', '4'}, {'7', '5'}, {'7', '6'}, {'7', '7'},
        {'7', '8'}, {'7', '9'}, {'8', '0'}, {'8', '1'}, {'8', '2'}, {'8', '3'},
        {'8', '4'}, {'8', '5'}, {'8', '6'}, {'8', '7'}, {'8', '8'}, {'8', '9'},
        {'9', '0'}, {'9', '1'}, {'9', '2'}, {'9', '3'}, {'9', '4'}, {'9', '5'},
        {'9', '6'}, {'9', '7'}, {'9', '8'}, {'9', '9'},
    }};

    // special case for "0"
    if (number == 0)
    {
        out += '0';
        return;
    }

    // use a pointer to fill the buffer
    auto* bufferPtr = numberbuffer.begin();

    const bool isNegative = std::is_same<NumberType, int64_t>::value &&
                            !(number >= 0); // see issue #755
    uint64_t absValue = 0;

    unsigned int nChars = 0;

    if (isNegative)
    {
        *bufferPtr = '-';
        absValue = static_cast<uint64_t>(0 - number);

        // account one more byte for the minus sign
        nChars = 1 + countDigits(absValue);
    }
    else
    {
        absValue = static_cast<uint64_t>(number);
        nChars = countDigits(absValue);
    }

    // spare 1 byte for '\0'
    if (nChars >= numberbuffer.size() - 1)
    {
        return;
    }

    // jump to the end to generate the string from backward
    // so we later avoid reversing the result
    std::advance(bufferPtr, nChars - 1);

    // Fast int2ascii implementation inspired by "Fastware" talk by Andrei
    // Alexandrescu See: https://www.youtube.com/watch?v=o4-CwDo2zpg
    while (absValue >= 100)
    {
        const auto digitsIndex = static_cast<unsigned>((absValue % 100));
        absValue /= 100;
        *bufferPtr = digitsTo99[digitsIndex][1];
        bufferPtr = std::prev(bufferPtr);
        *bufferPtr = digitsTo99[digitsIndex][0];
        bufferPtr = std::prev(bufferPtr);
    }

    if (absValue >= 10)
    {
        const auto digitsIndex = static_cast<unsigned>(absValue);
        *bufferPtr = digitsTo99[digitsIndex][1];
        bufferPtr = std::prev(bufferPtr);
        *bufferPtr = digitsTo99[digitsIndex][0];
        // assignment never used: bufferPtr = std::prev(bufferPtr);
    }
    else
    {
        *bufferPtr = static_cast<char>('0' + absValue);
        // assignment never used: bufferPtr = std::prev(bufferPtr);
    }

    out.append(numberbuffer.data(), nChars);
}

static void dumpfloat(std::string& out, double number)
{
    // NaN / inf
    if (!std::isfinite(number))
    {
        out += "null";
        return;
    }
    std::array<char, 64> numberbuffer{{}};

    ::nlohmann::detail::to_chars(numberbuffer.begin(), numberbuffer.end(),
                                 number);

    out += numberbuffer.data();
}

static void dump(std::string& out, const nlohmann::json& val)
{
    switch (val.type())
    {
        case nlohmann::json::value_t::object:
        {
            if (val.empty())
            {
                out += "{}";
                return;
            }

            out += "{";

            out += "<div class=tab>";
            for (auto i = val.begin(); i != val.end();)
            {
                out += "&quot";
                dumpEscaped(out, i.key());
                out += "&quot: ";

                bool inATag = false;
                if (i.key() == "@odata.id" || i.key() == "@odata.context" ||
                    i.key() == "Members@odata.nextLink" || i.key() == "Uri")
                {
                    inATag = true;
                    out += "<a href=\"";
                    dumpEscaped(out, i.value());
                    out += "\">";
                }
                dump(out, i.value());
                if (inATag)
                {
                    out += "</a>";
                }
                i++;
                if (i != val.end())
                {
                    out += ",";
                }
                out += "<br>";
            }
            out += "</div>";
            out += '}';

            return;
        }

        case nlohmann::json::value_t::array:
        {
            if (val.empty())
            {
                out += "[]";
                return;
            }

            out += "[";

            out += "<div class=tab>";

            // first n-1 elements
            for (auto i = val.cbegin(); i != val.cend() - 1; ++i)
            {
                dump(out, *i);
                out += ",<br>";
            }

            // last element
            dump(out, val.back());

            out += "</div>";
            out += ']';

            return;
        }

        case nlohmann::json::value_t::string:
        {
            out += '\"';
            const std::string* ptr = val.get_ptr<const std::string*>();
            if (ptr == nullptr)
            {
                return;
            }
            dumpEscaped(out, *ptr);
            out += '\"';
            return;
        }

        case nlohmann::json::value_t::boolean:
        {
            if (*(val.get_ptr<const bool*>()))
            {
                out += "true";
            }
            else
            {
                out += "false";
            }
            return;
        }

        case nlohmann::json::value_t::number_integer:
        {
            dumpInteger(out, *(val.get_ptr<const int64_t*>()));
            return;
        }

        case nlohmann::json::value_t::number_unsigned:
        {
            dumpInteger(out, *(val.get_ptr<const uint64_t*>()));
            return;
        }

        case nlohmann::json::value_t::number_float:
        {
            dumpfloat(out, *(val.get_ptr<const double*>()));
            return;
        }

        case nlohmann::json::value_t::discarded:
        {
            out += "<discarded>";
            return;
        }

        case nlohmann::json::value_t::null:
        {
            out += "null";
            return;
        }
        default:
        {
            // Do nothing;  Should never happen.
            return;
        }
    }
}

void dumpHtml(std::string& out, const nlohmann::json& json)
{
    out += "<html>\n"
           "<head>\n"
           "<title>Redfish API</title>\n"
           "<link href=\"/styles/redfish.css\" rel=\"stylesheet\">\n"
           "</head>\n"
           "<body>\n"
           "<div class=\"container\">\n"
           "<img src=\"/images/DMTF_Redfish_logo_2017.svg\" alt=\"redfish\" "
           "height=\"406px\" "
           "width=\"576px\">\n"
           "<div class=\"content\">\n";
    dump(out, json);
    out += "</div>\n"
           "</div>\n"
           "</body>\n"
           "</html>\n";
}

void prettyPrintJson(crow::Response& res)
{
    std::string html;
    json_html_util::dumpHtml(html, res.jsonValue);

    res.write(std::move(html));
    res.addHeader(boost::beast::http::field::content_type,
                  "text/html;charset=UTF-8");
}

} // namespace json_html_util