#include "tile/base/encoding/base64.h"

#include "tile/base/encoding/detail/base64_decode.inc"
#include "tile/base/encoding/detail/base64_encode.inc"
#include "tile/base/encoding/detail/base64_impl.h"
#include "tile/base/internal/logging.h"
#include "tile/base/internal/meta.h"
#include "tile/base/likely.h"

#include <algorithm>

namespace tile {
namespace {
#define BLOCK_SIZE_4K (4096)
#define BLOCK_SIZE_64K (65536)
#define BLOCK_SIZE_1M (1048576)
static constexpr std::size_t kBufferSize = BLOCK_SIZE_1M;
#undef BLOCK_SIZE_4K
#undef BLOCK_SIZE_64K
#undef BLOCK_SIZE_1M

}// namespace

std::string
EncodeBase64(Slice from)
{
    std::string result;
    EncodeBase64(from, &result);
    return result;
}

std::optional<std::string>
DecodeBase64(Slice from)
{
    std::string result;
    if (!DecodeBase64(from, &result)) { return std::nullopt; }
    return result;
}

void
EncodeBase64(Slice from, std::string *to)
{
    TILE_CHECK(from.size() < std::numeric_limits<int>::max(),
               "Not implemented: Source bytes too long. {} > max_size({})", from.size(),
               std::numeric_limits<int>::max());

    const auto size = 4 * ((from.size() + 2) / 3);
    to->resize(size);
    base64_encodestate state;
    base64_init_encodestate(&state);

    std::size_t used = 0;
    std::size_t input_len;
    while (!from.empty()) {
        input_len = std::min(kBufferSize, from.size());
        used += base64_encode_block(reinterpret_cast<const char *>(from.data()), input_len,
                                    reinterpret_cast<char *>(internal::RemoveConstPtr(to->data())) + used, &state);
        from.RemovePrefix(input_len);
    };
    base64_encode_blockend(reinterpret_cast<char *>(internal::RemoveConstPtr(to->data())) + used, &state);

    // bool success =
    //     Base64::Encode(from.data(), from.size(),
    //                    internal::RemoveConstPtr(to->data()), to->size());
    // TILE_CHECK(success, "Unexpected: Failed to do base64-encode.");
}

bool
DecodeBase64(Slice from, std::string *to)
{
    if (TILE_UNLIKELY(from.empty())) {
        to->clear();
        return true;
    }

    std::string safe_from_str;
    // auto padding
    auto padding = (4 - from.size() % 4) % 4;
    if (padding > 2) {
        return false;
    } else if (padding != 0) {
        TILE_LOG_WARNING("`from.size()` [{}] Can not be divisible by 4. "
                         "auto add padding [{}] `=`",
                         from.size(), padding);
        safe_from_str = from.ToString() + std::string(padding, '=');
        from          = safe_from_str;
    }

    auto size = 3 * from.size() / 4;
    to->resize(size);

    {
        Slice input = from;
        base64_decodestate state;
        base64_init_decodestate(&state);
        std::size_t used = 0;
        while (!input.empty()) {
            auto input_len   = std::min(kBufferSize, from.size());
            auto encoded_len = base64_decode_block(
                input.data(), input_len, reinterpret_cast<char *>(internal::RemoveConstPtr(to->data())) + used, &state);
            if (TILE_UNLIKELY(encoded_len == 0)) { return false; }
            used += encoded_len;
            input.RemovePrefix(input_len);
        }
    }

    // === Sample
    // {
    //   bool success =
    //       Base64::Decode(from.data(), from.size(),
    //                      internal::RemoveConstPtr(to->data()), to->size());
    //   if (!success) {
    //     return false;
    //   }
    // }

    // === OpenSSL
    // int rc;
    // rc = EVP_DecodeBlock(
    //     reinterpret_cast<unsigned char
    //     *>(internal::RemoveConstPtr(to->data())), reinterpret_cast<const
    //     unsigned char *>(from.data()), from.size());
    // if (rc != size) {
    //   return false;
    // }
    //
    // @sa: https://tools.ietf.org/html/rfc4648#section-4
    //
    // (1) The final quantum of encoding input is an integral multiple of 24
    //     bits; here, the final unit of encoded output will be an integral
    //     multiple of 4 characters with no "=" padding.
    //
    // (2) The final quantum of encoding input is exactly 8 bits; here, the
    //     final unit of encoded output will be two characters followed by
    //     two "=" padding characters.
    //
    // (3) The final quantum of encoding input is exactly 16 bits; here, the
    //     final unit of encoded output will be three characters followed by
    //     one "=" padding character.
    TILE_CHECK(from.size() >= 2 && to->size() >= 2);
    if (from[from.size() - 1] == '=') { to->pop_back(); }

    if (from[from.size() - 2] == '=') { to->pop_back(); }

    // to->pop_back(); // Remove Terminating null

    return true;
}

}// namespace tile
