//
// LDASTools frameCPP - A library implementing the LIGO/Virgo frame
// specification
//
// Copyright (C) 2018 California Institute of Technology
//
// LDASTools frameCPP is free software; you may redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 (GPLv2) of the
// License or at your discretion, any later version.
//
// LDASTools frameCPP is distributed in the hope that it will be useful, but
// without any warranty or even the implied warranty of merchantability
// or fitness for a particular purpose. See the GNU General Public
// License (GPLv2) for more details.
//
// Neither the names of the California Institute of Technology (Caltech),
// The Massachusetts Institute of Technology (M.I.T), The Laser
// Interferometer Gravitational-Wave Observatory (LIGO), nor the names
// of its contributors may be used to endorse or promote products derived
// from this software without specific prior written permission.
//
// You should have received a copy of the licensing terms for this
// software included in the file LICENSE located in the top-level
// directory of this package. If you did not, you can view a copy at
// http://dcc.ligo.org/M1500244/LICENSE
//

#include <framecpp_config.h>
#include <cstring>
#include <iomanip>
#include <iostream>
#include "framecpp/Common/MD5Sum.hh"

using std::memcpy;

#if HAVE_OPENSSL_MD5_H && HAVE_MD5_IN_CRYPTO
extern "C" {
#if !defined( SWIGIMPORTED )
#include <openssl/md5.h>
#include <openssl/evp.h>
#include <openssl/opensslv.h>
#endif
} // extern "C"
#endif // OPENSSL_MD5_H

#ifndef MD5_DIGEST_LENGTH
#define MD5_DIGEST_LENGTH 16
#endif

namespace MD5Wrapper {

/// A wrapper structure to manage MD5 context and operations.
/// This structure abstracts the MD5 context handling to support
/// both OpenSSL 3.0 and earlier versions by switching between
/// EVP and legacy MD5 APIs as appropriate.
struct MD5ContextWrapper {
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
    EVP_MD_CTX* context; ///< Context for OpenSSL 3.0+ EVP MD5 operations.
#else
    MD5_CTX context; ///< Context for legacy MD5 operations.
#endif

    /// Constructor to initialize the MD5 context.
    MD5ContextWrapper() {
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
        context = EVP_MD_CTX_new();
        EVP_DigestInit_ex(context, EVP_md5(), nullptr);
#else
        MD5_Init(&context);
#endif
    }

    /// Destructor to release the MD5 context.
    ~MD5ContextWrapper() {
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
        EVP_MD_CTX_free(context);
#endif
    }

    /// Initializes the MD5 context for a new hash computation.
    inline void init() {
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
        EVP_DigestInit_ex(context, EVP_md5(), nullptr);
#else
        MD5_Init(&context);
#endif
    }

    /// Updates the MD5 context with data for hashing.
    /// @param data Pointer to the data to hash.
    /// @param length Length of the data in bytes.
    inline void update(const unsigned char* data, size_t length) {
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
        EVP_DigestUpdate(context, data, length);
#else
        MD5_Update(&context, data, length);
#endif
    }

    /// Finalizes the MD5 hash computation and retrieves the digest.
    /// @param digest Buffer to store the resulting hash.
    inline void finalize(unsigned char* digest) {
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
        EVP_DigestFinal_ex(context, digest, nullptr);
#else
        MD5_Final(digest, &context);
#endif
    }

    /// Copies the state of another MD5 context into this one.
    /// @param other The MD5 context to copy from.
    inline MD5ContextWrapper& operator=(const MD5ContextWrapper& other) {
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
        EVP_MD_CTX_copy_ex(context, other.context);
#else
        memcpy(&context, &other.context, sizeof(context));
#endif
        return *this;
    }

    /// Compares two MD5 contexts for equality.
    /// @param other The other MD5 context to compare with.
    bool operator==(const MD5ContextWrapper& other) const {
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
        return false; // Cannot directly compare EVP_MD_CTX objects
#else
        return (memcmp(&context, &other.context, sizeof(context)) == 0);
#endif
    }
};

} // namespace MD5Wrapper

namespace FrameCPP
{
    namespace Common
    {
        /// Internal structure to manage the state of the MD5 sum computation.
        struct MD5Sum::_state_type
        {
            bool final; ///< Indicates whether the hash computation is finalized.
            MD5Wrapper::MD5ContextWrapper context; ///< The MD5 context wrapper.
            CHAR_U message_digest[MD5_DIGEST_LENGTH]; ///< The computed MD5 digest.

            /// Constructor initializes the MD5 context.
            inline _state_type() : final(false) { context.init(); }

            /// Copy constructor.
            /// @param Source The state to copy from.
            inline _state_type(const _state_type& Source) : final(Source.final), context(Source.context) {
                memcpy(message_digest, Source.message_digest, sizeof(message_digest));
            }

            /// Assignment operator to copy from another state.
            /// @param Source The state to copy from.
            inline _state_type& operator=(const _state_type& Source) {
                final = Source.final;
                context = Source.context;
                memcpy(message_digest, Source.message_digest, sizeof(message_digest));
                return *this;
            }
        };

        /// Constructor for MD5Sum, initializes a new MD5 context.
        MD5Sum::MD5Sum() : state(new _state_type) {}

        /// Finalizes the MD5 hash computation.
        void MD5Sum::Finalize() const {
            if (!state->final) {
                state->context.finalize(state->message_digest);
                state->final = true;
            }
        }

        /// Checks if the hash computation is finalized.
        /// @return True if finalized, otherwise false.
        inline bool MD5Sum::Finalized() const { return state->final; }

        /// Resets the MD5 context for a new hash computation.
        void MD5Sum::Reset() { state->context.init(); }

        /// Updates the MD5 hash with new data.
        /// @param Data Pointer to the data to hash.
        /// @param Length Length of the data in bytes.
        /// @throws std::runtime_error if called after finalization.
        void MD5Sum::Update(const data_type Data, size_type Length) {
            if (state->final) {
                throw std::runtime_error("MD5 error: Update called after value had been finalized");
            }
            state->context.update(static_cast<const unsigned char*>(Data), Length);
        }

        /// Retrieves the finalized MD5 digest.
        /// @return Pointer to the MD5 digest.
        MD5Sum::md_type MD5Sum::Value() const {
            Finalize();
            return static_cast<md_type>(state->message_digest);
        }
    } // namespace Common
} // namespace FrameCPP

/// Copy constructor for MD5Sum.
/// @param Source The MD5Sum instance to copy from.
FrameCPP::Common::MD5Sum::MD5Sum(const MD5Sum& Source) : state(new _state_type) {
    *state = *Source.state;
}

/// Dumps intermediate MD5 state to an output stream.
/// @param Stream The stream to output to.
/// @return Reference to the modified output stream.
std::ostream& FrameCPP::Common::MD5Sum::DumpIntermediate(std::ostream& Stream) const {
    std::ios_base::fmtflags sflags(Stream.flags());
    Stream.setf(std::ios_base::hex, std::ios_base::basefield);
    Stream.unsetf(std::ios_base::uppercase);
    std::streamsize swidth(Stream.width(8));
    int sfill(Stream.fill('0'));

    Stream.width(swidth);
    Stream.fill(sfill);
    Stream.flags(sflags);

    return Stream;
}

/// Assignment operator for MD5Sum.
/// @param Source The MD5Sum instance to copy from.
/// @return Reference to this MD5Sum.
FrameCPP::Common::MD5Sum& FrameCPP::Common::MD5Sum::operator=(const MD5Sum& Source) {
    *state = *Source.state;
    return *this;
}

/// Equality operator to compare two MD5Sum objects.
/// @param RHS The other MD5Sum instance to compare with.
/// @return True if both MD5Sum objects are equal; otherwise false.
bool FrameCPP::Common::MD5Sum::operator==(const MD5Sum& RHS) const {
    bool retval = false;
    if (&RHS == this) {
        retval = true;
    } else if (Finalized() == RHS.Finalized()) {
        if (Finalized()) {
            retval = std::equal(state->message_digest, state->message_digest + MD5_DIGEST_LENGTH, RHS.state->message_digest);
        } else {
            retval = (state->context == RHS.state->context);
        }
    }
    return retval;
}

/// Outputs the MD5Sum object as a hexadecimal string.
/// @param Stream The output stream.
/// @param Data The MD5Sum object.
/// @return Reference to the output stream.
std::ostream& std::operator<<(std::ostream& Stream, const FrameCPP::Common::MD5Sum& Data) {
    Data.Finalize();

    std::ios_base::fmtflags sflags(Stream.flags());
    Stream.setf(std::ios_base::hex, std::ios_base::basefield);
    Stream.unsetf(std::ios_base::uppercase);
    std::streamsize swidth(Stream.width(2));
    int sfill(Stream.fill('0'));

    for (INT_2U i = 0; i < sizeof(Data.state->message_digest); ++i) {
        Stream << std::hex << std::setw(2) << std::setfill('0') << (int)Data.state->message_digest[i];
    }

    Stream.width(swidth);
    Stream.fill(sfill);
    Stream.flags(sflags);

    return Stream;
}
