/*
 * 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.
 */

#ifndef INCLUDED_BUILDBOXCOMMON_DIGESTGENERATOR
#define INCLUDED_BUILDBOXCOMMON_DIGESTGENERATOR

#include <buildboxcommon_logging.h>
#include <buildboxcommon_protos.h>

#include <openssl/evp.h>
#include <string>

namespace buildboxcommon {

class DigestContext;

class DigestGenerator {
  public:
    static void init(const DigestFunction_Value digestFunctionValue =
                         static_cast<DigestFunction_Value>(
                             BUILDBOXCOMMON_DIGEST_FUNCTION_VALUE));

    static inline bool initialized() { return s_initialized; }

    /** resets digest function, ONLY FOR TESTS */
    static void resetState();

    /**
     * Return a Digest corresponding to the contents of the given file
     * descriptor.
     */
    static Digest hash(int fd);

    /**
     * Return a Digest corresponding to the given string.
     */
    static Digest hash(const std::string &data);

    static Digest hash(const google::protobuf::MessageLite &message);

    /**
     * Return a Digest corresponding to the contents of the file in `path`.
     *
     */
    static Digest hashFile(const std::string &path);

    /**
     * Return a `DigestFunction` message specifying the hash function used.
     */
    static inline DigestFunction_Value digestFunction()
    {
        if (!s_initialized) {
            BUILDBOXCOMMON_THROW_EXCEPTION(std::runtime_error,
                                           "digest function not initialized");
        }
        return s_digestFunctionValue;
    };

    static DigestContext createDigestContext();

    static inline const std::unordered_set<DigestFunction_Value> &
    supportedDigestFunctions()
    {
        return s_supportedDigestFunctions;
    }

    static std::string supportedDigestFunctionsList();

    static DigestFunction_Value
    stringToDigestFunction(const std::string &functionName);

  private:
    static DigestFunction_Value s_digestFunctionValue;
    static bool s_initialized;

    static const std::unordered_set<DigestFunction_Value>
        s_supportedDigestFunctions;

    // Size of the buffer used to read files from disk. Determines the
    // number of bytes that will be read in each chunk.
    static const size_t HASH_BUFFER_SIZE_BYTES;
};

class DigestContext {
  public:
    virtual ~DigestContext();

    // Finish calculating a digest and generate the result.
    Digest finalizeDigest();

    // Rule of Five: Delete copy constructor and copy assignment operator
    DigestContext(const DigestContext &) = delete;
    DigestContext &operator=(const DigestContext &) = delete;

    // Rule of Five: Define move constructor and move assignment operator
    DigestContext(DigestContext &&other) noexcept;
    DigestContext &operator=(DigestContext &&other) noexcept;

    // Calculate the hash of a portion of a file. This allows to read a
    // file from disk in chunks to avoid storing it wholly in memory.
    void update(const char *data, size_t data_size);

    static DigestContext generate(DigestFunction_Value digestFunctionValue);

  private:
    EVP_MD_CTX *d_context;
    size_t d_data_size = 0;
    bool d_finalized = false;

    // Create and initialize an OpenSSL digest context to be used during a
    // call to `hash()`.
    DigestContext();
    void init(const EVP_MD *digestFunctionStruct);

    // If `status_code` is 0, throw an `std::runtime_error` exception with
    // a description containing `function_name`. Otherwise, do nothing.
    // (Note that this considers 0 an error, following the OpenSSL
    // convention.)
    static void throwIfNotSuccessful(int status_code,
                                     const std::string &function_name);

    // Take a hash value produced by OpenSSL and return a string with
    // its representation in hexadecimal.
    static std::string hashToHex(const unsigned char *hash_buffer,
                                 unsigned int hash_size);

    friend class DigestGenerator;
};

} // namespace buildboxcommon

#endif
