
#include "EncodeAndDecodeUtils.h"


extern "C" {
#include "zlib.h"
}
#include <regex>
#include <stdexcept>
#include <algorithm>
#include <cctype>
#include <cstdint>

namespace {
    static const std::string base64_chars =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
        "abcdefghijklmnopqrstuvwxyz"
        "0123456789+/";

    inline bool is_base64(unsigned char c) {
        return (std::isalnum(c) || (c == '+') || (c == '/'));
    }
}

std::string EncodeAndDecodeUtils::Base64Encode(const std::vector<unsigned char>& data) {
    std::string ret;
    ret.reserve(((data.size() + 2) / 3) * 4);

    size_t i = 0;
    const size_t len = data.size();
    while (i + 2 < len) {
        unsigned int n = (data[i] << 16) + (data[i + 1] << 8) + data[i + 2];
        ret.push_back(base64_chars[(n >> 18) & 63]);
        ret.push_back(base64_chars[(n >> 12) & 63]);
        ret.push_back(base64_chars[(n >> 6) & 63]);
        ret.push_back(base64_chars[n & 63]);
        i += 3;
    }

    if (i < len) {
        unsigned int n = data[i] << 16;
        ret.push_back(base64_chars[(n >> 18) & 63]);
        if (i + 1 < len) {
            n += data[i + 1] << 8;
            ret.push_back(base64_chars[(n >> 12) & 63]);
            ret.push_back(base64_chars[(n >> 6) & 63]);
            ret.push_back('=');
        } else {
            ret.push_back(base64_chars[(n >> 12) & 63]);
            ret.push_back('=');
            ret.push_back('=');
        }
    }

    return ret;
}

std::vector<unsigned char> EncodeAndDecodeUtils::Base64Decode(const std::string& input) {
    std::vector<unsigned char> out;
    size_t in_len = input.size();
    size_t i = 0;
    int val = 0, valb = -8;

    out.reserve((in_len * 3) / 4);

    for (unsigned char c : input) {
        if (std::isspace(c)) continue;
        if (c == '=') break;
        if (!is_base64(c)) continue;
        val = (val << 6) + static_cast<int>(base64_chars.find(c));
        valb += 6;
        if (valb >= 0) {
            out.push_back(static_cast<unsigned char>((val >> valb) & 0xFF));
            valb -= 8;
        }
    }

    // Handle padding chars '='
    size_t pad = 0;
    if (!input.empty()) {
        if (input.back() == '=') pad++;
        if (input.size() > 1 && input[input.size() - 2] == '=') pad++;
    }

    // Note: above loop already wrote correct bytes; pad does not require extra handling here

    return out;
}

std::string EncodeAndDecodeUtils::UnEncodeAndUnGzipString(const std::string& encodedToken) {
    auto compressed = Base64Decode(encodedToken);
    if (compressed.empty()) return std::string();

    // initialize zlib for gzip decoding (16 + MAX_WBITS to enable gzip decoding)
    z_stream strm;
    std::memset(&strm, 0, sizeof(strm));
    strm.next_in = const_cast<Bytef*>(reinterpret_cast<const Bytef*>(compressed.data()));
    strm.avail_in = static_cast<uInt>(compressed.size());

    int ret = inflateInit2(&strm, 16 + MAX_WBITS);
    if (ret != Z_OK) {
        inflateEnd(&strm);
        throw std::runtime_error("inflateInit2 failed");
    }

    std::vector<unsigned char> out;
    const size_t CHUNK = 4096;
    out.reserve(compressed.size() * 2);

    unsigned char buffer[CHUNK];
    do {
        strm.next_out = buffer;
        strm.avail_out = static_cast<uInt>(CHUNK);

        ret = inflate(&strm, Z_NO_FLUSH);
        if (ret != Z_OK && ret != Z_STREAM_END) {
            inflateEnd(&strm);
            throw std::runtime_error("inflate failed during decompression");
        }

        size_t have = CHUNK - strm.avail_out;
        if (have > 0) {
            out.insert(out.end(), buffer, buffer + have);
        }
    } while (ret != Z_STREAM_END);

    inflateEnd(&strm);

    return std::string(out.begin(), out.end());
}

std::string EncodeAndDecodeUtils::GzipAndBase64EncodeString(const std::string& json) {
    const unsigned char* data = reinterpret_cast<const unsigned char*>(json.data());
    size_t data_len = json.size();

    z_stream strm;
    std::memset(&strm, 0, sizeof(strm));
    strm.next_in = const_cast<Bytef*>(data);
    strm.avail_in = static_cast<uInt>(data_len);

    // windowBits = 16 + MAX_WBITS -> gzip header/trailer
    int ret = deflateInit2(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 16 + MAX_WBITS, 8, Z_DEFAULT_STRATEGY);
    if (ret != Z_OK) {
        deflateEnd(&strm);
        throw std::runtime_error("deflateInit2 failed");
    }

    std::vector<unsigned char> out;
    const size_t CHUNK = 4096;
    unsigned char buffer[CHUNK];

    do {
        strm.next_out = buffer;
        strm.avail_out = static_cast<uInt>(CHUNK);

        ret = deflate(&strm, Z_FINISH); // compress all input in one go
        if (ret != Z_OK && ret != Z_STREAM_END && ret != Z_BUF_ERROR) {
            deflateEnd(&strm);
            throw std::runtime_error("deflate failed");
        }

        size_t have = CHUNK - strm.avail_out;
        if (have > 0) {
            out.insert(out.end(), buffer, buffer + have);
        }
    } while (ret != Z_STREAM_END);

    deflateEnd(&strm);

    return Base64Encode(out);
}

std::string EncodeAndDecodeUtils::FixupGzipEolCraziness(const std::string& newJsonText) {
    std::string s = newJsonText;

    // Normalize CRLF -> LF
    size_t pos = 0;
    while ((pos = s.find("\r\n", pos)) != std::string::npos) {
        s.replace(pos, 2, "\n");
        pos += 1;
    }

    // Normalize remaining CR -> LF
    pos = 0;
    while ((pos = s.find('\r', pos)) != std::string::npos) {
        s.replace(pos, 1, "\n");
        pos += 1;
    }

    // Remove sequences of blank lines (one or more whitespace-only lines) to a single newline
    try {
        std::regex re("\n\\s*\n", std::regex_constants::ECMAScript | std::regex_constants::icase);
        s = std::regex_replace(s, re, "\n");
    }
    catch (const std::regex_error&) {
        // If regex fails for any reason, fallback to a simpler loop that collapses double newlines a few times.
        for (int i = 0; i < 3; ++i) {
            size_t p = 0;
            while ((p = s.find("\n\n", p)) != std::string::npos) {
                s.erase(p, 1); // remove one of the duplicate newlines
            }
        }
    }

    // Final pass to ensure no repeated blank lines remain
    size_t p = 0;
    while ((p = s.find("\n\n", p)) != std::string::npos) {
        s.erase(p, 1);
    }

    return s;
}
