/*
 * Copyright 2019 Bloomberg Finance LP
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <buildboxcommon_digestgenerator.h>

#include <buildboxcommon_exception.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_logging.h>

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <cstddef>

namespace buildboxcommon {

const size_t DigestGenerator::HASH_BUFFER_SIZE_BYTES = 1024UL * 64UL;

const std::unordered_set<DigestFunction_Value>
    DigestGenerator::s_supportedDigestFunctions = {
        DigestFunction_Value_MD5, DigestFunction_Value_SHA1,
        DigestFunction_Value_SHA256, DigestFunction_Value_SHA384,
        DigestFunction_Value_SHA512};

#ifndef BUILDBOXCOMMON_DIGEST_FUNCTION_VALUE
#error "Digest function not defined"
#endif

DigestFunction_Value DigestGenerator::s_digestFunctionValue =
    DigestFunction_Value_UNKNOWN;
bool DigestGenerator::s_initialized = false;

void DigestGenerator::init(const DigestFunction_Value digestFunctionValue)
{
    if (s_initialized) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                       "Digest function already initialized");
    }
    std::unordered_set<DigestFunction_Value> supported = {
        DigestFunction_Value_MD5, DigestFunction_Value_SHA1,
        DigestFunction_Value_SHA256, DigestFunction_Value_SHA384,
        DigestFunction_Value_SHA512};
    if (supported.find(digestFunctionValue) == supported.end()) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error,
            "Digest function not supported: "
                << DigestFunction_Value_Name(digestFunctionValue));
    }
    s_digestFunctionValue = digestFunctionValue;
    s_initialized = true;
}

void DigestGenerator::resetState()
{
    s_digestFunctionValue = DigestFunction_Value_UNKNOWN;
    s_initialized = false;
}

DigestContext DigestGenerator::createDigestContext()
{
    return DigestContext::generate(digestFunction());
}

Digest DigestGenerator::hash(int fd)
{
    DigestContext context = createDigestContext();
    std::array<char, HASH_BUFFER_SIZE_BYTES> buffer{};

    lseek(fd, 0, SEEK_SET);

    ssize_t bytes_read = 0;
    while ((bytes_read = read(fd, buffer.data(), buffer.size())) > 0) {
        context.update(buffer.data(), static_cast<size_t>(bytes_read));
    }
    if (bytes_read == -1) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error in read on file descriptor " << fd);
    }

    return context.finalizeDigest();
}

Digest DigestGenerator::hash(const std::string &data)
{
    DigestContext context = createDigestContext();
    context.update(data.c_str(), data.size());
    return context.finalizeDigest();
}

Digest DigestGenerator::hash(const google::protobuf::MessageLite &message)
{
    return hash(message.SerializeAsString());
}

Digest DigestGenerator::hashFile(const std::string &path)
{
    const FileDescriptor fd(open(path.c_str(), O_RDONLY | O_CLOEXEC));
    if (fd.get() == -1) {
        BUILDBOXCOMMON_THROW_SYSTEM_EXCEPTION(
            std::system_error, errno, std::system_category,
            "Error opening file \"" << path << "\"");
    }

    return hash(fd.get());
}

DigestContext DigestContext::generate(DigestFunction_Value digestFunctionValue)
{
    const EVP_MD *digestFunctionStruct = nullptr;
    switch (digestFunctionValue) {
        case DigestFunction_Value_MD5:
            digestFunctionStruct = EVP_md5();
            break;
        case DigestFunction_Value_SHA1:
            digestFunctionStruct = EVP_sha1();
            break;

        case DigestFunction_Value_SHA256:
            digestFunctionStruct = EVP_sha256();
            break;

        case DigestFunction_Value_SHA384:
            digestFunctionStruct = EVP_sha384();
            break;

        case DigestFunction_Value_SHA512:
            digestFunctionStruct = EVP_sha512();
            break;

        default:
            BUILDBOXCOMMON_THROW_EXCEPTION(
                std::runtime_error, "Digest function value not supported: "
                                        << digestFunctionValue);
    };
    DigestContext context;
    context.init(digestFunctionStruct);
    return context;
}

void DigestContext::update(const char *data, size_t data_size)
{
    if (d_finalized) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                       "Cannot update finalized digest");
    }

    throwIfNotSuccessful(EVP_DigestUpdate(d_context, data, data_size),
                         "EVP_DigestUpdate()");

    d_data_size += data_size;
}

Digest DigestContext::finalizeDigest()
{
    if (d_finalized) {
        BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                       "Digest already finalized");
    }

    unsigned char hash_buffer[EVP_MAX_MD_SIZE];

    unsigned int message_length = 0;
    throwIfNotSuccessful(
        EVP_DigestFinal_ex(d_context, hash_buffer, &message_length),
        "EVP_DigestFinal_ex()");

    d_finalized = true;

    const std::string hash = hashToHex(hash_buffer, message_length);

    Digest digest;
    digest.set_hash(hash);
    digest.set_size_bytes(static_cast<google::protobuf::int64>(d_data_size));
    return digest;
}

std::string DigestContext::hashToHex(const unsigned char *hash_buffer,
                                     unsigned int hash_size)
{
    std::ostringstream ss;
    for (unsigned int i = 0; i < hash_size; i++) {
        ss << std::hex << std::setw(2) << std::setfill('0')
           << static_cast<int>(
                  hash_buffer // NOLINT
                              // (cppcoreguidelines-pro-bounds-pointer-arithmetic)
                      [i]);
    }
    return ss.str();
}

void DigestContext::throwIfNotSuccessful(int status_code,
                                         const std::string &function_name)
{
    if (status_code == 0) {
        throw std::runtime_error(function_name + " failed.");
    }
    // "EVP_DigestInit_ex(), EVP_DigestUpdate() and EVP_DigestFinal_ex() return
    // 1 for success and 0 for failure."
    // https://openssl.org/docs/man1.1.0/man3/EVP_DigestInit.html
}

DigestContext::DigestContext() : d_context(EVP_MD_CTX_create())
{

    if (!d_context) {
        BUILDBOXCOMMON_THROW_EXCEPTION(
            std::runtime_error, "Error creating `EVP_MD_CTX` context struct");
    }
}

void DigestContext::init(const EVP_MD *digestFunctionStruct)
{
    throwIfNotSuccessful(
        EVP_DigestInit_ex(d_context, digestFunctionStruct, nullptr),
        "EVP_DigestInit_ex()");
}

DigestContext::~DigestContext()
{
    EVP_MD_CTX_destroy(d_context);
    // ^ Calling this macro ensures compatibility with OpenSSL 1.0.2:
    // "EVP_MD_CTX_create() and EVP_MD_CTX_destroy() were
    // renamed to EVP_MD_CTX_new() and EVP_MD_CTX_free() in OpenSSL 1.1."
    // (https://openssl.org/docs/man1.1.0/man3/EVP_DigestInit.html)
}

DigestContext::DigestContext(DigestContext &&other) noexcept
    : d_context(other.d_context), d_data_size(other.d_data_size),
      d_finalized(other.d_finalized)
{
    other.d_context = nullptr;
    other.d_data_size = 0;
    other.d_finalized = false;
}

DigestContext &DigestContext::operator=(DigestContext &&other) noexcept
{
    if (this != &other) {
        if (d_context) {
            EVP_MD_CTX_free(d_context);
        }
        d_context = other.d_context;
        d_data_size = other.d_data_size;
        d_finalized = other.d_finalized;

        other.d_context = nullptr;
        other.d_data_size = 0;
        other.d_finalized = false;
    }
    return *this;
}

std::string DigestGenerator::supportedDigestFunctionsList()
{
    const auto &set = s_supportedDigestFunctions;
    std::string str;

    auto it = set.cbegin();
    while (it != set.cend()) {
        DigestFunction_Value f = *it;
        str += DigestFunction_Value_Name(f);

        it++;

        if (it != set.cend()) {
            str += ", ";
        }
    }

    return str;
}

DigestFunction_Value
DigestGenerator::stringToDigestFunction(const std::string &functionName)
{
    const auto &set = s_supportedDigestFunctions;
    auto it = set.cbegin();
    while (it != set.cend()) {
        DigestFunction_Value df = *it;
        if (functionName == DigestFunction_Value_Name(df)) {
            return df;
        }
        it++;
    }
    BUILDBOXCOMMON_THROW_EXCEPTION(
        std::runtime_error, "digest function not supported: " + functionName);
}
} // namespace buildboxcommon
