/**
 * @file mega/logging.h
 * @brief Logging class
 *
 * (c) 2013-2014 by Mega Limited, Auckland, New Zealand
 *
 * This file is part of the MEGA SDK - Client Access Engine.
 *
 * Applications using the MEGA API must present a valid application key
 * and comply with the the rules set forth in the Terms of Service.
 *
 * The MEGA SDK is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 *
 * @copyright Simplified (2-clause) BSD License.
 *
 * You should have received a copy of the license along with this
 * program.
 *
 * This file is also distributed under the terms of the GNU General
 * Public License, see http://www.gnu.org/copyleft/gpl.txt for details.
 */

/* Usage example:

   1)
    #include <fstream>  // for std::ofstream

    // output debug messages to file
    std::ofstream debugfile;
    debugfile.open("debug.log");

    // use logDebug level
    SimpleLogger::setLogLevel(logDebug);
    // set output to stdout for all messages
    SimpleLogger::setAllOutputs(&std::cout);

    // add file for debug output only
    SimpleLogger::addOutput(logDebug, &debugfile);
    // print additional components for debug output
    SimpleLogger::setOutputSettings(logDebug, true, true, true);

    ...
    LOG_debug << "test"; // will print message on screen and append line to debugfile
    LOG_info << "informing"; // will only print message on screen


    2)
    // set output class for all types of logs (for example - send logs to remote Log Server)
    class MyOutput: public Logger {
    public:
        void log(const char *time, int loglevel, const char *source, const char *message) {
            std::cout << "{" << time << "}" << " [" << source << "] " << message << std::endl;
        }
    };

    ...
    MyOutput output;

    // let's output both Debug (verbose) and Info (just messages) logs
    // and send logs to remote Log Server
    SimpleLogger::setOutputSettings(logDebug, true, true, true);
    SimpleLogger::setOutputSettings(logInfo, false, false, false);
    SimpleLogger::setLogLevel(logDebug);
    SimpleLogger::setAllOutputs(&std::cout);
    SimpleLogger::setOutputClass(output);
    SimpleLogger::setOutputClass(&myOutput);

    LOG_debug << "test";
    LOG_info << "informing";


    3)
    if MEGA_QT_LOGGING defined:

    QString a = QString::fromAscii("test1");
    LOG_info << a;
    LOG_info << QString::fromAscii("test2");

    4) Performance mode can be enabled via defining ENABLE_LOG_PERFORMANCE at compile time.

    In performance mode, the `SimpleLogger` does not lock mutexes nor does it heap-allocate.
    Only `loglevel` and `message` of the `Logger` are populated where `message` will include
    file/line. It is assumed that the subclass of `Logger` provides timing information.

    In performance mode, only outputting to a logger assigned through `setOutputClass` is supported.
    Output streams are not supported.
*/
#pragma once

#include <algorithm>
#include <array>
#include <atomic>
#include <cassert>
#include <cstring>
#include <iostream>
#include <mutex>
#include <sstream>
#include <string>
#include <vector>
#if __cplusplus >= 202002L
#include <filesystem>
#endif

// define MEGA_QT_LOGGING to support QString
#ifdef MEGA_QT_LOGGING
#ifdef __GNUC__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wconversion"
#endif
#include <QString>
#ifdef __GNUC__
#pragma GCC diagnostic pop
#endif
#endif

#include "mega/log_level.h"
#include "mega/utils.h"

namespace mega {

// Output Log Interface
class Logger
{
public:
    virtual ~Logger() = default;
    // Note: `time` and `source` are null in performance mode
    virtual void log(const char *time, int loglevel, const char *source, const char *message
#ifdef ENABLE_LOG_PERFORMANCE
          , const char **directMessages = nullptr, size_t *directMessagesSizes = nullptr, unsigned numberMessages = 0
#endif
                     ) = 0;
};

const static size_t LOGGER_CHUNKS_SIZE = 1024;

/**
 * @brief holds a const char * and its size to pass to SimpleLogger, to use the direct logging logic
 */
class DirectMessage{

private:
    const static size_t directMsgThreshold = 1024; //below this, the msg will be buffered as a normal message
    bool mForce = false;
    size_t mSize = 0;
    const char *mConstChar = nullptr;

public:

    DirectMessage( const char *constChar, bool force = false) //force will set size as max, so as to stay above directMsgThreshold
    {
        mConstChar = constChar;
        mSize = strlen(constChar);
        mForce = force;
    }

    template<typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
    DirectMessage( const char *constChar, T size, bool force = false)
    {
        mConstChar = constChar;
        mSize = static_cast<size_t>(size);
        mForce = force;
    }

    size_t size() const
    {
        return mSize;
    }

    const char *constChar() const
    {
        return mConstChar;
    }
};

// Describes one chunk of a larger streamed body for logging. If the total body size
// (totalSize) exceeds maxLogSize, SimpleLogger will only emit the slices of this chunk that
// overlap with the head window [0, maxLogSize/2) and tail window [totalSize - maxLogSize/2,
// totalSize); bytes in between are replaced by a "[...]" marker. If totalSize is negative
// (unknown), only the head window is emitted. If totalSize <= maxLogSize, the full chunk is
// emitted.
struct ChunkedDirectMessage
{
    const char* data;
    size_t chunkSize;
    size_t chunkOffset;
    int64_t totalSize;
    size_t maxLogSize;
};

class SimpleLogger
{
    LogLevel level;

#ifndef ENABLE_LOG_PERFORMANCE
    std::ostringstream ostr;
    std::string t;
    std::string fname;

    std::string getTime();
#else
    using Buffer = std::array<char, LOGGER_CHUNKS_SIZE>;
    static inline thread_local Buffer mBuffer;
    static inline thread_local Buffer::iterator mBufferIt{mBuffer.begin()};

    using DiffType = std::array<char, LOGGER_CHUNKS_SIZE>::difference_type;
    using NumBuf = char[24];
    const char* filenameStr;
    int lineNum;

    // True if there is a previous instance in the logging chain.
    // For example, the first LOG_debug below will have isChained == false:
    // the second LOG_debug inside f() below will have isChained == true:
    //     int f() {LOG_DEBUG << "not first in chain"; return 2;}
    //     LOG_debug << "1" << f();
    bool isChained{false};
    static inline thread_local std::vector<DirectMessage> mDirectMessages;
    static inline thread_local std::vector<std::string> mCopiedParts;

    template<typename DataIterator>
    void copyToBuffer(const DataIterator dataIt, DiffType currentSize)
    {
        if (mThreadLocalLoggingDisabled) return;

        DiffType start = 0;
        while (currentSize > 0)
        {
            const auto size = std::min(currentSize, std::distance(mBufferIt, mBuffer.end() - 1));
            mBufferIt = std::copy(dataIt + start, dataIt + start + size, mBufferIt);
            if (mBufferIt == mBuffer.end() - 1)
            {
                outputBuffer();
            }
            start += size;
            currentSize -= size;
        }
    }

    void outputBuffer(bool lastcall = false)
    {
        if (mThreadLocalLoggingDisabled) return;

        *mBufferIt = '\0';
        if (!mDirectMessages.empty()) // some part has already been passed as direct, we'll do all directly
        {
            if (lastcall) //the mBuffer can be reused
            {
                mDirectMessages.push_back(DirectMessage(mBuffer.data(), std::distance(mBuffer.begin(), mBufferIt)));
            }
            else //reached LOGGER_CHUNKS_SIZE, we need to copy mBuffer contents
            {
                mCopiedParts.emplace_back(string(mBuffer.data()));
                mDirectMessages.push_back(
                    DirectMessage{mCopiedParts.back().data(), mCopiedParts.back().size()});
            }
        }
        else
        {
            Logger* currentLogger = logger.load(std::memory_order_acquire);
            if (currentLogger)
            {
                currentLogger->log(nullptr, level, nullptr, mBuffer.data());
            }
        }
        mBufferIt = mBuffer.begin();
    }

    template<typename T>
    void logValue(const std::atomic<T>& value)
    {
        logValue(value.load());
    }

    template<typename T>
    typename std::enable_if<std::is_enum<T>::value>::type
    logValue(const T value)
    {
        NumBuf buf;
        const auto size = snprintf(buf, sizeof(buf), "%d", static_cast<int>(value));
        copyToBuffer(buf, std::min(size, static_cast<int>(sizeof(buf)) - 1));
    }

    template<typename T>
    typename std::enable_if<std::is_pointer<T>::value && !std::is_same<T, char*>::value>::type
    logValue(const T value)
    {
        NumBuf buf;
        const auto size = snprintf(buf, sizeof(buf), "%p", reinterpret_cast<const void*>(value));
        copyToBuffer(buf, std::min(size, static_cast<int>(sizeof(buf)) - 1));
    }

    template<typename T>
    typename std::enable_if<std::is_integral<T>::value && std::is_signed<T>::value
                            && !std::is_same<T, long>::value && !std::is_same<T, long long>::value>::type
    logValue(const T value)
    {
        NumBuf buf;
        const auto size = snprintf(buf, sizeof(buf), "%d", value);
        copyToBuffer(buf, std::min(size, static_cast<int>(sizeof(buf)) - 1));
    }

    template<typename T>
    typename std::enable_if<std::is_integral<T>::value && std::is_signed<T>::value
                            && std::is_same<T, long>::value && !std::is_same<T, long long>::value>::type
    logValue(const T value)
    {
        NumBuf buf;
        const auto size = snprintf(buf, sizeof(buf), "%ld", value);
        copyToBuffer(buf, std::min(size, static_cast<int>(sizeof(buf)) - 1));
    }

    template<typename T>
    typename std::enable_if<std::is_integral<T>::value && std::is_signed<T>::value
                            && !std::is_same<T, long>::value && std::is_same<T, long long>::value>::type
    logValue(const T value)
    {
        NumBuf buf;
        const auto size = snprintf(buf, sizeof(buf), "%lld", value);
        copyToBuffer(buf, std::min(size, static_cast<int>(sizeof(buf)) - 1));
    }

    template<typename T>
    typename std::enable_if<std::is_integral<T>::value && std::is_unsigned<T>::value
                            && !std::is_same<T, unsigned long>::value && !std::is_same<T, unsigned long long>::value>::type
    logValue(const T value)
    {
        NumBuf buf;
        const auto size = snprintf(buf, sizeof(buf), "%u", value);
        copyToBuffer(buf, std::min(size, static_cast<int>(sizeof(buf)) - 1));
    }

    template<typename T>
    typename std::enable_if<std::is_integral<T>::value && std::is_unsigned<T>::value
                            && std::is_same<T, unsigned long>::value && !std::is_same<T, unsigned long long>::value>::type
    logValue(const T value)
    {
        NumBuf buf;
        const auto size = snprintf(buf, sizeof(buf), "%lu", value);
        copyToBuffer(buf, std::min(size, static_cast<int>(sizeof(buf)) - 1));
    }

    template<typename T>
    typename std::enable_if<std::is_integral<T>::value && std::is_unsigned<T>::value
                            && !std::is_same<T, unsigned long>::value && std::is_same<T, unsigned long long>::value>::type
    logValue(const T value)
    {
        NumBuf buf;
        const auto size = snprintf(buf, sizeof(buf), "%llu", value);
        copyToBuffer(buf, std::min(size, static_cast<int>(sizeof(buf)) - 1));
    }

    template<typename T>
    typename std::enable_if<std::is_floating_point<T>::value>::type
    logValue(const T value)
    {
        NumBuf buf;
        const auto size = snprintf(buf, sizeof(buf), "%g", value);
        copyToBuffer(buf, std::min(size, static_cast<int>(sizeof(buf)) - 1));
    }

    void logValue(const char* value)
    {
        copyToBuffer(value, static_cast<DiffType>(std::strlen(value)));
    }

    void logValue(const std::string& value)
    {
        copyToBuffer(value.begin(), static_cast<DiffType>(value.size()));
    }

    void logValue(const std::string_view value)
    {
        copyToBuffer(value.begin(), static_cast<DiffType>(value.size()));
    }

    void logValue(const ::mega::Error& value)   // ::mega:: when building MEGAchat on windows, else ambiguity errors
    {
        logValue(error(value));
    }

    void logValue(const std::error_code& value)
    {
        logValue(value.category().name());
        logValue(":");
        logValue(value.message());
    }

    void logValue(const std::system_error& se)
    {
        logValue(se.code().category().name());
        logValue(": ");
        logValue(se.what());
    }
#endif

    static std::atomic<Logger*> logger;

    static std::atomic<LogLevel> logCurrentLevel;

    static constexpr size_t PAYLOAD_LOG_DEFAULT_SIZE = 10240;

    inline static std::atomic_size_t maxPayloadLogSize = PAYLOAD_LOG_DEFAULT_SIZE;

public:
    // flag to turn off logging on the log-output thread, to prevent possible deadlock cycles.
    static inline thread_local bool mThreadLocalLoggingDisabled = false;

    SimpleLogger(const LogLevel ll,
                 const char* filename,
                 const int line,
                 [[maybe_unused]] bool noChainAssert = false):
        level{ll}
#ifdef ENABLE_LOG_PERFORMANCE
        ,
        filenameStr(filename),
        lineNum(line),
        isChained(false)
#endif
    {
        if (mThreadLocalLoggingDisabled) return;

#ifdef ENABLE_LOG_PERFORMANCE

        // isChained if there are logs in the mBuffer or mDirectMessages
        // Note: such as if the first logger instance doesn't output anything and we treat
        // the second one as not in chain
        isChained = mBufferIt != mBuffer.begin() || !mDirectMessages.empty();
        // logging chain is not well supported, change caller code to avoid it
        assert(!isChained || noChainAssert);
#else
        Logger* currentLogger = logger.load(std::memory_order_acquire);
        if (!currentLogger)
        {
            return;
        }

        t = getTime();
        std::ostringstream oss;
        oss << filename;
        if(line >= 0)
        {
            oss << ":" << line;
        }
        fname = oss.str();
#endif
    }

    ~SimpleLogger()
    {
        if (mThreadLocalLoggingDisabled) return;

#ifdef ENABLE_LOG_PERFORMANCE
        if (filenameStr && lineNum != -1)
        {
            copyToBuffer(" [", 2);
            logValue(filenameStr);  // put filename and line last, to keep the main text nicely column aligned
            copyToBuffer(":", 1);
            logValue(lineNum);
            copyToBuffer("]", 1);
        }

        // The last output if it is not chained
        const auto lastOutput = !isChained;
        outputBuffer(lastOutput);

        // Direct Messages are log on last output
        if (lastOutput)
        {
            if (!mDirectMessages.empty())
            {
                Logger* currentLogger = logger.load(std::memory_order_acquire);
                if (currentLogger)
                {
                    std::unique_ptr<const char*[]> dm(new const char*[mDirectMessages.size()]);
                    std::unique_ptr<size_t[]> dms(new size_t[mDirectMessages.size()]);
                    unsigned i = 0;
                    for (const auto& d: mDirectMessages)
                    {
                        dm[i] = d.constChar();
                        dms[i] = d.size();
                        i++;
                    }

                    currentLogger->log(nullptr, level, nullptr, "", dm.get(), dms.get(), i);
                }
            }
            // Clear
            mDirectMessages.clear();
            mCopiedParts.clear();
        }

#else
        Logger* currentLogger = logger.load(std::memory_order_acquire);
        if (currentLogger)
        {
            const auto message = ostr.str();
            currentLogger
                ->log(/*                                                                     */
                      t.c_str(), /* time stamp                                               */
                      level, /* log level                                                    */
                      fname.c_str(), /* file name and line                                   */
                      message.c_str()); /* the log message itself                            */
        }
#endif
    }

    SimpleLogger(const SimpleLogger&) = delete;
    SimpleLogger& operator=(const SimpleLogger&) = delete;

    static const char *toStr(LogLevel ll)
    {
        switch (ll)
        {
            case logDebug: return "debug";
            case logInfo: return "info";
            case logWarning: return "warn";
            case logError: return "err";
            case logFatal: return "FATAL";
            case logVerbose: return "verbose";
        }
        assert(false);
        return "";
    }

    template <typename T>
    SimpleLogger& operator<<(T* obj)
    {
#ifdef ENABLE_LOG_PERFORMANCE
        if (obj)
        {
            logValue(obj);
        }
        else
        {
            copyToBuffer("(NULL)", 6);
        }
#else
        if (obj)
        {
            ostr << obj;
        }
        else
        {
            ostr << "(NULL)";
        }
#endif
        return *this;
    }

    template <typename T, typename = typename std::enable_if<std::is_scalar<T>::value>::type>
    SimpleLogger& operator<<(const T obj)
    {
        static_assert(!std::is_same<T, std::nullptr_t>::value, "T cannot be nullptr_t");
#ifdef ENABLE_LOG_PERFORMANCE
        logValue(obj);
#else
        ostr << obj;
#endif
        return *this;
    }

    template<typename T>
    typename std::enable_if<!std::is_scalar<T>::value
#if defined(__cpp_char8_t) || __cplusplus >= 202002L
                                && !std::is_same<T, std::u8string>::value
#endif
#if __cplusplus >= 202002L
                                && !std::is_same<T, std::filesystem::path>::value
#endif
                            ,
                            SimpleLogger&>::type
        operator<<(const T& obj)
    {
#ifdef ENABLE_LOG_PERFORMANCE
        logValue(obj);
#else
        ostr << obj;
#endif
        return *this;
    }

#ifdef MEGA_QT_LOGGING
    SimpleLogger& operator<<(const QString& s)
    {
#ifdef ENABLE_LOG_PERFORMANCE
        logValue(s.toUtf8().constData());
#else
        ostr << s.toUtf8().constData();
#endif
        return *this;
    }
#endif

#if defined(__cpp_char8_t) || __cplusplus >= 202002L
    // Overload for std::u8string (char8_t string) - convert to std::string for streaming
    // Available when char8_t is supported
    SimpleLogger& operator<<(const std::u8string& s)
    {
#ifdef ENABLE_LOG_PERFORMANCE
        logValue(std::string(reinterpret_cast<const char*>(s.data()), s.size()));
#else
        ostr << std::string(reinterpret_cast<const char*>(s.data()), s.size());
#endif
        return *this;
    }
#endif

#if __cplusplus >= 202002L
    // Overload for std::filesystem::path - convert to std::string for streaming
    SimpleLogger& operator<<(const std::filesystem::path& p)
    {
#ifdef ENABLE_LOG_PERFORMANCE
        logValue(p.string());
#else
        ostr << p.string();
#endif
        return *this;
    }
#endif

    template <typename T>
    SimpleLogger& operator<<(const std::unique_ptr<T>& ptr)
    {
#ifdef ENABLE_LOG_PERFORMANCE
        if (!ptr)
        {
            logValue("<empty unique ptr>");
        }
        else
        {
            logValue(*ptr.get());
        }
#else
        if (!ptr)
        {
            ostr << "<empty unique ptr>";
        }
        else
        {
            ostr << *ptr.get();
        }
#endif
        return *this;
    }

    template <typename T>
    SimpleLogger& operator<<(const std::shared_ptr<T>& ptr)
    {
#ifdef ENABLE_LOG_PERFORMANCE
        if (!ptr)
        {
            logValue("<empty shared ptr>");
        }
        else
        {
            logValue(*ptr.get());
        }
#else
        if (!ptr)
        {
            ostr << "<empty shared ptr>";
        }
        else
        {
            ostr << *ptr.get();
        }
#endif
        return *this;
    }

    SimpleLogger& operator<<(const DirectMessage &obj)
    {
#ifndef ENABLE_LOG_PERFORMANCE
        ostr.write(obj.constChar(), static_cast<std::streamsize>(obj.size()));
#else
        // careful using constChar() without taking size() into account: *this << obj.constChar(); ended up with 2MB+ lines from fetchnodes.

        if (mBufferIt != mBuffer.begin()) //something was appended to the buffer before this direct msg
        {
            *mBufferIt = '\0';

            mCopiedParts.emplace_back(string(mBuffer.data()));
            mDirectMessages.push_back(
                DirectMessage{mCopiedParts.back().data(), mCopiedParts.back().size()});

            mBufferIt = mBuffer.begin();
        }

        mDirectMessages.push_back(DirectMessage(obj.constChar(), obj.size()));
#endif
        return *this;
    }

    SimpleLogger& operator<<(const ChunkedDirectMessage& obj)
    {
        // 1. Handle Unknown Size
        if (obj.totalSize < 0)
        {
            if (obj.chunkOffset >= obj.maxLogSize)
            {
                return *this << "[...]";
            }
            size_t len = std::min(obj.chunkSize, obj.maxLogSize - obj.chunkOffset);
            *this << DirectMessage(obj.data, len);
            if (len < obj.chunkSize)
            {
                *this << "[...]";
            }
            return *this;
        }

        // 2. Handle Small Data (No need to truncate)
        if (static_cast<size_t>(obj.totalSize) <= obj.maxLogSize)
        {
            return *this << DirectMessage(obj.data, obj.chunkSize);
        }

        // 3. Handle Truncation (Head + Tail)
        const size_t headEnd = obj.maxLogSize / 2;
        const size_t tailStart = static_cast<size_t>(obj.totalSize) - obj.maxLogSize / 2;

        // Helper to calculate overlap
        auto getOverlap = [&](size_t start, size_t end) -> std::pair<size_t, size_t>
        {
            size_t s = std::max(obj.chunkOffset, start);
            size_t e = std::min(obj.chunkOffset + obj.chunkSize, end);
            return (s < e) ? std::make_pair(s - obj.chunkOffset, e - s) :
                             std::make_pair((size_t)0, (size_t)0);
        };

        auto [hOff, hLen] = getOverlap(0, headEnd);
        auto [tOff, tLen] = getOverlap(tailStart, static_cast<size_t>(obj.totalSize));

        if (hLen > 0)
        {
            *this << DirectMessage(obj.data + hOff, hLen);
        }

        // Emit "[...]" if any byte of this chunk lies in the middle gap [headEnd, tailStart).
        const size_t chunkEnd = obj.chunkOffset + obj.chunkSize;
        if (std::max(obj.chunkOffset, headEnd) < std::min(chunkEnd, tailStart))
        {
            *this << "[...]";
        }

        if (tLen > 0)
        {
            *this << DirectMessage(obj.data + tOff, tLen);
        }

        return *this;
    }

    // set output class
    static void setOutputClass(Logger *logger_class)
    {
        logger.store(logger_class, std::memory_order_release);
    }

    // set the current log level. all logs which are higher than this level won't be handled
    static void setLogLevel(LogLevel ll)
    {
        logCurrentLevel = ll;
    }
    inline static LogLevel getLogLevel()
    {
        return logCurrentLevel;
    }

    /**
     * @brief Sets the maximum size limit for request payload logging.
     *
     * When logging request payloads, messages exceeding this size will be truncated
     * with "[...]" to prevent extremely large log entries.
     *
     * @param size Maximum payload size in bytes. If set to 0, the max size limit of size_t is
     * applied and payloads will be logged in full regardless of size. Default is
     * PAYLOAD_LOG_DEFAULT_SIZE (10240 bytes).
     *
     */
    static void setMaxPayloadLogSize(size_t size = PAYLOAD_LOG_DEFAULT_SIZE)
    {
        if (size == 0)
            maxPayloadLogSize = std::numeric_limits<size_t>::max();
        else
            maxPayloadLogSize = size;
    }

    inline static size_t getMaxPayloadLogSize()
    {
        return maxPayloadLogSize;
    }

    // Log messages forwarded from the client app though the configured logging mechanisms.
    // These do not go through the LOG_<level> macros.
    //
    // When ENABLE_LOG_PERFORMANCE is on, this must not be called during the lifetime of an
    // existing instance created in the same thread. Otherwise logs are mixed. That would be
    // a rare case, but could be avoided by writing:
    // LOG_info << "foo", SimpleLogger::postLog(logInfo, "bar", filename, line);
    static void postLog(LogLevel logLevel, const char *message, const char *filename, int line)
    {
        if (logCurrentLevel < logLevel) return;
        SimpleLogger simpleLogger(logLevel, filename ? filename : "", line);
        if (message)
            simpleLogger << message;
    }
};

// source file leaf name - maybe to be compile time calculated one day
template<std::size_t N> inline const char* log_file_leafname( const char (&fullpath)[N]) {
    for (auto i = N - 1; --i; )
    {
        if (fullpath[i] == '/' || fullpath[i] == '\\')
            return &fullpath[i+1];
    }
    return fullpath;
}

std::ostream& operator <<(std::ostream&, const std::system_error&);
std::ostream& operator <<(std::ostream&, const std::error_code&);

// Helper used in LOG_* macros below to make the right operand of ?: void to match the left one
struct LoggerVoidify
{
    void operator&(SimpleLogger&) {}
};

// Note: If the argument to LOG_level includes any function call, it may be executed TWICE
#define LOG_level(LOG_LEVEL) \
    ::mega::SimpleLogger::getLogLevel() < (LOG_LEVEL) ? \
        (void)0 : \
        ::mega::LoggerVoidify() & \
            ::mega::SimpleLogger((LOG_LEVEL), ::mega::log_file_leafname(__FILE__), __LINE__)

#define LOG_verbose LOG_level(::mega::logVerbose)
#define LOG_debug LOG_level(::mega::logDebug)
#define LOG_info LOG_level(::mega::logInfo)
#define LOG_warn LOG_level(::mega::logWarning)
#define LOG_err LOG_level(::mega::logError)
#define LOG_fatal \
    ::mega::SimpleLogger(::mega::logFatal, ::mega::log_file_leafname(__FILE__), __LINE__)

/**
 * @brief Checks if the current time is within an active time window.
 *
 * This function uses static timing to manage cycles of active and rest periods, determining if the
 * current call is made within an active time span.
 *
 * @tparam Duration The type of the chrono duration e.g. std::chrono::seconds, std::chrono::minutes.
 * @param sleepDuration The duration of the inactivity period.
 * @param activeDuration The duration of the active period.
 */
template<typename TimeUnit>
inline bool isWithinActivePeriod(const TimeUnit sleepDuration, const TimeUnit activeDuration)
{
    using namespace std::chrono;
    static const auto startTime = steady_clock::now();

    const auto elapsed = duration_cast<TimeUnit>(steady_clock::now() - startTime);

    const TimeUnit period = sleepDuration + activeDuration;
    const TimeUnit currentPhase = elapsed % period;

    return currentPhase >= sleepDuration && currentPhase < period;
}

#define LOG_generic_timed(LOG_LEVEL, SLEEP_DUR, ACTIVE_DUR) \
::mega::SimpleLogger::getLogLevel() < LOG_LEVEL || !isWithinActivePeriod(SLEEP_DUR, ACTIVE_DUR) ? \
    (void)0 : \
    ::mega::LoggerVoidify() & \
        ::mega::SimpleLogger(LOG_LEVEL, ::mega::log_file_leafname(__FILE__), __LINE__)

#define LOG_verbose_timed(SLEEP, ACTIVE) LOG_generic_timed(::mega::logVerbose, SLEEP, ACTIVE)
#define LOG_debug_timed(SLEEP, ACTIVE) LOG_generic_timed(::mega::logDebug, SLEEP, ACTIVE)
#define LOG_info_timed(SLEEP, ACTIVE) LOG_generic_timed(::mega::logInfo, SLEEP, ACTIVE)
#define LOG_warn_timed(SLEEP, ACTIVE) LOG_generic_timed(::mega::logWarning, SLEEP, ACTIVE)
#define LOG_err_timed(SLEEP, ACTIVE) LOG_generic_timed(::mega::logError, SLEEP, ACTIVE)
#define LOG_fatal_timed(SLEEP, ACTIVE) LOG_generic_timed(::mega::logFatal, SLEEP, ACTIVE)

#define MEGA_ASSERT(cond, streamArgs) \
    do \
    { \
        const bool megaAssert_ = static_cast<bool>(cond); \
        if (!megaAssert_) \
        { \
            LOG_err << streamArgs; \
            assert(megaAssert_&& #cond); \
        } \
    } \
    while (0)

// moved from the intermediate layer
class ExternalLogger : public Logger
{
public:

    typedef std::function<
        void(const char *time, int loglevel, const char *source, const char *message
#ifdef ENABLE_LOG_PERFORMANCE
            , const char **directMessages, size_t *directMessagesSizes, unsigned numberMessages
#endif
            )> LogCallback;

    ExternalLogger();
    ~ExternalLogger();
    void addMegaLogger(void* id, LogCallback);
    void removeMegaLogger(void* id);
    void setLogLevel(int logLevel);
    void setLogToConsole(bool enable);
    void log(const char *time, int loglevel, const char *source, const char *message
#ifdef ENABLE_LOG_PERFORMANCE
        , const char **directMessages, size_t *directMessagesSizes, unsigned numberMessages
#endif
    ) override;

    // Do not use this unless you know what you are doing!
    //
    // This is an unfortunate workaround for cases when multiple connections/clients add
    // each its own logger to the same target (i.e. file), leading to duplicated messages
    // being logged consecutively.
    // This for example has happened in MegaChat's automated tests.
    void useOnlyFirstLogger(bool onlyFirst = true) { useOnlyFirstMegaLogger = onlyFirst; }

private:
    std::recursive_mutex mutex;
    map<void*, LogCallback> megaLoggers;
    bool logToConsole;
    bool alreadyLogging = false;
    bool useOnlyFirstMegaLogger = false;
};


class ExclusiveLogger : public Logger
{
    // A lock-free adapter for loggers that require not to lock the mutex (e.g. RotativePerformanceLogger)
    // Note: we are using this being extra precautiuos: we don't let these loggers to work with any other external loggers. Hence the Exclusive.

public:

    typedef std::function<
        void(const char *time, int loglevel, const char *source, const char *message
#ifdef ENABLE_LOG_PERFORMANCE
            , const char **directMessages, size_t *directMessagesSizes, unsigned numberMessages
#endif
            )> LogCallback;

    void log(const char *time, int loglevel, const char *source, const char *message
#ifdef ENABLE_LOG_PERFORMANCE
        , const char **directMessages, size_t *directMessagesSizes, unsigned numberMessages
#endif
    ) override;

    LogCallback exclusiveCallback;
};


// This used to be a static member of MegaApi_impl
// However, megacli could not use or test it from there since it
// uses the SDK core directly, and not the intermediate layer
// So, although globals and singletons are not ideal, moving it here
// is one step forwards in tidying that up.

// Update: Changing from Global variables to singleton is
//         for solving the race conditions in multi-threaded environment.
// extern ExternalLogger g_externalLogger;
// extern ExclusiveLogger g_exclusiveLogger;

/*
 * @brief Get the External Logger object
 *
 * @return ExternalLogger&
 */
ExternalLogger& getExternalLogger();

/*
 * @brief Get the Exclusive Logger object
 *
 * @return ExclusiveLogger&
 */
ExclusiveLogger& getExclusiveLogger();

inline error logAndReturnError(const error e, const std::string_view msg)
{
    LOG_err << msg;
    return e;
}

/**
 * @brief A generic version of `logAndReturnError` where you can specify how the return value is
 * instantiated from the error code
 *
 * @param returnGenerator A function that can be invoked with `const error` and generates the
 * returned value
 * @return The result from invoking returnGenerator(e)
 */
template<typename F>
auto logAndReturnErrorGeneric(const error e, const std::string_view msg, F&& returnGenerator)
    -> std::invoke_result_t<F, error>
{
    LOG_err << msg;
    return returnGenerator(e);
}

/**
 * @brief A helper factory function to generate callables with the same input parameters as
 * `logAndReturnError` but with a custom return type, generated by resGenerator.
 *
 * @example
 * const auto logAndError = generateLogAndReturnError(
 *     [](const error e) -> std::pair<error, std::pair<std::string, unsigned>>
 *     {
 *         return {e, {"", 0}};
 *     });
 * return logAndError(API_EARGS, "Invalid handle"); // will return the pair
 */
template<typename F>
constexpr auto generateLogAndReturnError(F&& resGenerator)
    -> std::function<std::invoke_result_t<std::decay_t<F>, error>(error, std::string_view)>
{
    return [gen = std::forward<F>(resGenerator)](const error e, const std::string_view msg)
    {
        return logAndReturnErrorGeneric(e, msg, gen);
    };
}

// Logging msg in full if its size msgSize is less than maxLogSize. Otherwise, logging first and
// last parts of the msg based on maxLogSize/2.
#define MaxDirectMessage(msg, msgSize, maxLogSize) \
    ((msgSize) < (maxLogSize) ? DirectMessage((msg), (msgSize)) : \
                                DirectMessage((msg), (maxLogSize / 2))) \
        << ((msgSize) < (maxLogSize) ? "" : "[...]") \
        << ((msgSize) < (maxLogSize) ? \
                "" : \
                DirectMessage((msg) + (msgSize) - (maxLogSize / 2), (maxLogSize / 2)))

class JSONLog
{
public:
    enum
    {
        NONE = 0,
        CHUNK_RECEIVED = 1,
        CHUNK_PROCESSING = 1 << 1,
        CHUNK_CONSUMED = 1 << 2,
        SENDING = 1 << 3,
        NONCHUNK_RECEIVED = 1 << 4,
    };

    static uint32_t get()
    {
        return mJSONLog;
    }

    static void set(uint32_t value)
    {
        mJSONLog = value;
    }

private:
    inline static std::atomic_uint32_t mJSONLog{CHUNK_RECEIVED | CHUNK_CONSUMED | SENDING |
                                                NONCHUNK_RECEIVED};
};

#define JSON_CHUNK_RECEIVED \
    if (JSONLog::get() & JSONLog::CHUNK_RECEIVED) \
    LOG_debug

#define JSON_CHUNK_PROCESSING \
    if (JSONLog::get() & JSONLog::CHUNK_PROCESSING) \
    LOG_debug

#define JSON_CHUNK_CONSUMED \
    if (JSONLog::get() & JSONLog::CHUNK_CONSUMED) \
    LOG_debug

#define JSON_SENDING \
    if (JSONLog::get() & JSONLog::SENDING) \
    LOG_debug

#define JSON_NONCHUNK_RECEIVED \
    if (JSONLog::get() & JSONLog::NONCHUNK_RECEIVED) \
    LOG_debug
} // namespace
