/*
 * Copyright (c) 2024, Altomani Gianluca <altomanigianluca@gmail.com>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#pragma once

#include <AK/ByteString.h>
#include <AK/NonnullOwnPtr.h>
#include <AK/Types.h>
#include <LibCrypto/Hash/HashFunction.h>
#include <LibCrypto/OpenSSLForward.h>

namespace Crypto::Hash {

template<typename T, size_t BlockS, size_t DigestS, typename DigestT = Digest<DigestS>>
class OpenSSLHashFunction : public HashFunction<BlockS, DigestS, DigestT> {
    AK_MAKE_NONCOPYABLE(OpenSSLHashFunction);

public:
    using HashFunction<BlockS, DigestS, DigestT>::update;

    static NonnullOwnPtr<T> create()
    {
        auto* context = EVP_MD_CTX_new();
        return make<T>(context);
    }

    OpenSSLHashFunction(EVP_MD const* type, EVP_MD_CTX* context)
        : m_type(type)
        , m_context(context)
    {
        OpenSSLHashFunction::reset();
    }

    virtual ~OpenSSLHashFunction() override
    {
        EVP_MD_CTX_free(m_context);
    }

    virtual ByteString class_name() const override = 0;

    void update(u8 const* input, size_t length) override
    {
        if (EVP_DigestUpdate(m_context, input, length) != 1) {
            VERIFY_NOT_REACHED();
        }
    }

    DigestT digest() override
    {
        DigestT digest;
        if (EVP_DigestFinal_ex(m_context, digest.data, nullptr) != 1) {
            VERIFY_NOT_REACHED();
        }

        reset();
        return digest;
    }

    DigestT peek() override
    {
        auto c = copy();
        return c->digest();
    }

    void reset() override
    {
        if (EVP_DigestInit_ex(m_context, m_type, nullptr) != 1) {
            VERIFY_NOT_REACHED();
        }
    }

    virtual NonnullOwnPtr<T> copy() const
    {
        auto context = create();
        if (EVP_MD_CTX_copy_ex(context->m_context, m_context) != 1) {
            VERIFY_NOT_REACHED();
        }

        return context;
    }

    static DigestT hash(u8 const* data, size_t length)
    {
        auto hasher = create();
        hasher->update(data, length);
        return hasher->digest();
    }

    static DigestT hash(ByteBuffer const& buffer) { return hash(buffer.data(), buffer.size()); }
    static DigestT hash(StringView buffer) { return hash(reinterpret_cast<u8 const*>(buffer.characters_without_null_termination()), buffer.length()); }

private:
    EVP_MD const* m_type;
    EVP_MD_CTX* m_context;
};

}
