/***********************************************************************************************************************************
Log Handler
***********************************************************************************************************************************/
#include "build.auto.h"

#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <time.h>
#include <unistd.h>

#include "common/debug.h"
#include "common/log.h"
#include "common/macro.h"
#include "common/time.h"
#include "common/type/convert.h"

/***********************************************************************************************************************************
Module variables
***********************************************************************************************************************************/
// Log levels
static LogLevel logLevelStdOut = logLevelError;
static LogLevel logLevelStdErr = logLevelError;
static LogLevel logLevelFile = logLevelOff;
static LogLevel logLevelAny = logLevelError;

// Log file descriptors
static int logFdStdOut = STDOUT_FILENO;
static int logFdStdErr = STDERR_FILENO;
static int logFdFile = -1;

// Has the log file banner been written yet?
static bool logFileBanner = false;

// Is the timestamp printed in the log?
static bool logTimestamp = false;

// Default process id if none is specified
static unsigned int logProcessId = 0;

// Size of the process id field
static int logProcessSize = 2;

// Prefix DRY-RUN to log messages
static bool logDryRun = false;

/***********************************************************************************************************************************
Dry run prefix
***********************************************************************************************************************************/
#define DRY_RUN_PREFIX                                              "[DRY-RUN] "

/***********************************************************************************************************************************
Test Asserts
***********************************************************************************************************************************/
#define ASSERT_LOG_LEVEL(logLevel)                                                                                                 \
    ASSERT(logLevel >= LOG_LEVEL_MIN && logLevel <= LOG_LEVEL_MAX)

/***********************************************************************************************************************************
Log buffer -- used to format log header and message
***********************************************************************************************************************************/
static char logBuffer[LOG_BUFFER_SIZE];

/**********************************************************************************************************************************/
#define LOG_LEVEL_TOTAL                                             (LOG_LEVEL_MAX + 1)

static const struct LogLevel
{
    const StringId id;                                              // Id
    const char *const name;                                         // Name
} logLevelList[LOG_LEVEL_TOTAL] =
{
    {
        .id = STRID5("off", 0x18cf0),
        .name = "OFF",
    },
    {
        // No id here because this level is not user selectable
        .name = "ASSERT",
    },
    {
        .id = STRID5("error", 0x127ca450),
        .name = "ERROR",
    },
    {
        .id = STRID5("warn", 0x748370),
        .name = "WARN",
    },
    {
        .id = STRID5("info", 0x799c90),
        .name = "INFO",
    },
    {
        .id = STRID5("detail", 0x1890d0a40),
        .name = "DETAIL",
    },
    {
        .id = STRID5("debug", 0x7a88a40),
        .name = "DEBUG",
    },
    {
        .id = STRID5("trace", 0x5186540),
        .name = "TRACE",
    },
};

FN_EXTERN LogLevel
logLevelEnum(const StringId logLevelId)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(STRING_ID, logLevelId);
    FUNCTION_TEST_END();

    ASSERT(logLevelId != 0);

    LogLevel result = logLevelOff;

    // Search for the log level
    for (; result < LOG_LEVEL_TOTAL; result++)
        if (logLevelId == logLevelList[result].id)
            break;

    // Check that the log level was found
    CHECK(AssertError, result != LOG_LEVEL_TOTAL, "invalid log level");

    FUNCTION_TEST_RETURN(ENUM, result);
}

FN_EXTERN const char *
logLevelStr(const LogLevel logLevel)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, logLevel);
    FUNCTION_TEST_END();

    ASSERT(logLevel <= LOG_LEVEL_MAX);

    FUNCTION_TEST_RETURN_CONST(STRINGZ, logLevelList[logLevel].name);
}

/**********************************************************************************************************************************/
static void
logAnySet(void)
{
    FUNCTION_TEST_VOID();

    logLevelAny = logLevelStdOut;

    if (logLevelStdErr > logLevelAny)
        logLevelAny = logLevelStdErr;

    if (logLevelFile > logLevelAny && logFdFile != -1)
        logLevelAny = logLevelFile;

    FUNCTION_TEST_RETURN_VOID();
}

FN_EXTERN bool
logAny(const LogLevel logLevel)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, logLevel);
    FUNCTION_TEST_END();

    ASSERT_LOG_LEVEL(logLevel);

    FUNCTION_TEST_RETURN(BOOL, logLevel <= logLevelAny);
}

/**********************************************************************************************************************************/
FN_EXTERN void
logInit(
    const LogLevel logLevelStdOutParam, const LogLevel logLevelStdErrParam, const LogLevel logLevelFileParam,
    const bool logTimestampParam, const unsigned int processId, const unsigned int logProcessMax, const bool dryRunParam)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, logLevelStdOutParam);
        FUNCTION_TEST_PARAM(ENUM, logLevelStdErrParam);
        FUNCTION_TEST_PARAM(ENUM, logLevelFileParam);
        FUNCTION_TEST_PARAM(BOOL, logTimestampParam);
        FUNCTION_TEST_PARAM(UINT, processId);
        FUNCTION_TEST_PARAM(UINT, logProcessMax);
        FUNCTION_TEST_PARAM(BOOL, dryRunParam);
    FUNCTION_TEST_END();

    ASSERT(logLevelStdOutParam <= LOG_LEVEL_MAX);
    ASSERT(logLevelStdErrParam <= LOG_LEVEL_MAX);
    ASSERT(logLevelFileParam <= LOG_LEVEL_MAX);
    ASSERT(processId <= 999);
    ASSERT(logProcessMax <= 999);

    logLevelStdOut = logLevelStdOutParam;
    logLevelStdErr = logLevelStdErrParam;
    logLevelFile = logLevelFileParam;
    logTimestamp = logTimestampParam;
    logProcessId = processId;
    logProcessSize = logProcessMax > 99 ? 3 : 2;
    logDryRun = dryRunParam;

    logAnySet();

    FUNCTION_TEST_RETURN_VOID();
}

/***********************************************************************************************************************************
Close the log file
***********************************************************************************************************************************/
static void
logFileClose(void)
{
    FUNCTION_TEST_VOID();

    // Close the file descriptor if it is open
    if (logFdFile != -1)
    {
        close(logFdFile);
        logFdFile = -1;
    }

    logAnySet();

    FUNCTION_TEST_RETURN_VOID();
}

/**********************************************************************************************************************************/
FN_EXTERN bool
logFileSet(const char *const logFile)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(STRINGZ, logFile);
    FUNCTION_TEST_END();

    ASSERT(logFile != NULL);

    // Close the log file if it is already open
    logFileClose();

    // Only open the file if there is a chance to log something
    bool result = true;

    if (logLevelFile != logLevelOff)
    {
        // Open the file and handle errors
        logFdFile = open(logFile, O_CREAT | O_APPEND | O_WRONLY, 0640);

        if (logFdFile == -1)
        {
            const int errNo = errno;
            LOG_WARN_FMT(
                "unable to open log file '%s': %s\nNOTE: process will continue without log file.", logFile, strerror(errNo));
            result = false;
        }

        // Output the banner on first log message
        logFileBanner = false;

        logAnySet();
    }

    logAnySet();

    FUNCTION_TEST_RETURN(BOOL, result);
}

/**********************************************************************************************************************************/
FN_EXTERN void
logClose(void)
{
    FUNCTION_TEST_VOID();

    // Disable all logging
    logInit(logLevelOff, logLevelOff, logLevelOff, false, 0, 1, false);

    // Close the log file if it is open
    logFileClose();

    FUNCTION_TEST_RETURN_VOID();
}

/***********************************************************************************************************************************
Determine if the log level is in the specified range
***********************************************************************************************************************************/
static bool
logRange(const LogLevel logLevel, const LogLevel logRangeMin, const LogLevel logRangeMax)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, logLevel);
        FUNCTION_TEST_PARAM(ENUM, logRangeMin);
        FUNCTION_TEST_PARAM(ENUM, logRangeMax);
    FUNCTION_TEST_END();

    ASSERT_LOG_LEVEL(logLevel);
    ASSERT_LOG_LEVEL(logRangeMin);
    ASSERT_LOG_LEVEL(logRangeMax);
    ASSERT(logRangeMin <= logRangeMax);

    FUNCTION_TEST_RETURN(BOOL, logLevel >= logRangeMin && logLevel <= logRangeMax);
}

/***********************************************************************************************************************************
Internal write function that handles errors
***********************************************************************************************************************************/
static void
logWrite(const int fd, const char *const message, const size_t messageSize, const char *const errorDetail)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(INT, fd);
        FUNCTION_TEST_PARAM(STRINGZ, message);
        FUNCTION_TEST_PARAM(SIZE, messageSize);
        FUNCTION_TEST_PARAM(STRINGZ, errorDetail);
    FUNCTION_TEST_END();

    ASSERT(fd != -1);
    ASSERT(message != NULL);
    ASSERT(messageSize != 0);
    ASSERT(errorDetail != NULL);

    if ((size_t)write(fd, message, messageSize) != messageSize)
        THROW_SYS_ERROR_FMT(FileWriteError, "unable to write %s", errorDetail);

    FUNCTION_TEST_RETURN_VOID();
}

/***********************************************************************************************************************************
Write out log message and indent subsequent lines
***********************************************************************************************************************************/
static void
logWriteIndent(const int fd, const char *message, const size_t indentSize, const char *const errorDetail)
{
    // Indent buffer -- used to write out indent space without having to loop
    static const char indentBuffer[] = "                                                                                          ";

    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(INT, fd);
        FUNCTION_TEST_PARAM(STRINGZ, message);
        FUNCTION_TEST_PARAM(SIZE, indentSize);
        FUNCTION_TEST_PARAM(STRINGZ, errorDetail);
    FUNCTION_TEST_END();

    ASSERT(fd != -1);
    ASSERT(message != NULL);
    ASSERT(indentSize > 0 && indentSize < sizeof(indentBuffer));
    ASSERT(errorDetail != NULL);

    // Indent all lines after the first
    const char *linefeedPtr = strchr(message, '\n');
    bool first = true;

    while (linefeedPtr != NULL)
    {
        if (!first)
            logWrite(fd, indentBuffer, indentSize, errorDetail);
        else
            first = false;

        logWrite(fd, message, (size_t)(linefeedPtr - message + 1), errorDetail);
        message += (size_t)(linefeedPtr - message + 1);

        linefeedPtr = strchr(message, '\n');
    }

    FUNCTION_TEST_RETURN_VOID();
}

/***********************************************************************************************************************************
Generate the log header and anything else that needs to happen before the message is output
***********************************************************************************************************************************/
typedef struct LogPreResult
{
    size_t bufferPos;
    char *logBufferStdErr;
    size_t indentSize;
} LogPreResult;

static LogPreResult
logPre(
    const LogLevel logLevel, const unsigned int processId, const char *const fileName, const char *const functionName,
    const int code)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, logLevel);
        FUNCTION_TEST_PARAM(UINT, processId);
        FUNCTION_TEST_PARAM(STRINGZ, fileName);
        FUNCTION_TEST_PARAM(STRINGZ, functionName);
        FUNCTION_TEST_PARAM(INT, code);
    FUNCTION_TEST_END();

    ASSERT_LOG_LEVEL(logLevel);
    ASSERT(fileName != NULL);
    ASSERT(functionName != NULL);
    ASSERT(
        (code == 0 && logLevel > logLevelError) || (logLevel == logLevelError && code != errorTypeCode(&AssertError)) ||
        (logLevel == logLevelAssert && code == errorTypeCode(&AssertError)));

    // Initialize buffer position
    LogPreResult result = {.bufferPos = 0};

    // Add time
    if (logTimestamp)
    {
        const TimeMSec logTimeMSec = timeMSec();
        const time_t logTimeSec = (time_t)(logTimeMSec / MSEC_PER_SEC);

        result.bufferPos += cvtTimeToZP(
            "%Y-%m-%d %H:%M:%S", logTimeSec, logBuffer + result.bufferPos, sizeof(logBuffer) - result.bufferPos);
        result.bufferPos += (size_t)snprintf(
            logBuffer + result.bufferPos, sizeof(logBuffer) - result.bufferPos, ".%03d ", (int)(logTimeMSec % 1000));
    }

    // Add process and aligned log level
    result.bufferPos += (size_t)snprintf(
        logBuffer + result.bufferPos, sizeof(logBuffer) - result.bufferPos, "P%0*u %*s: ", logProcessSize,
        processId == (unsigned int)-1 ? logProcessId : processId, 6, logLevelStr(logLevel));

    // When writing to stderr the timestamp, process, and log level alignment will be skipped
    result.logBufferStdErr = logBuffer + result.bufferPos - strlen(logLevelStr(logLevel)) - 2;

    // Set the indent size -- this will need to be adjusted for stderr
    result.indentSize = result.bufferPos;

    // Add error code
    if (code != 0)
        result.bufferPos += (size_t)snprintf(logBuffer + result.bufferPos, sizeof(logBuffer) - result.bufferPos, "[%03d]: ", code);

    // Add dry-run prefix
    if (logDryRun)
        result.bufferPos += (size_t)snprintf(logBuffer + result.bufferPos, sizeof(logBuffer) - result.bufferPos, DRY_RUN_PREFIX);

    // Add debug info
    if (logLevel >= logLevelDebug)
    {
        // Adding padding for debug and trace levels. Cast to handle compilers (e.g. MSVC) that coerce to signed after subtraction.
        for (unsigned int paddingIdx = 0; paddingIdx < (unsigned int)((logLevel - logLevelDebug + 1) * 4); paddingIdx++)
        {
            logBuffer[result.bufferPos++] = ' ';
            result.indentSize++;
        }

        result.bufferPos += (size_t)snprintf(
            logBuffer + result.bufferPos, LOG_BUFFER_SIZE - result.bufferPos, "%.*s::%s: ", (int)strlen(fileName) - 2, fileName,
            functionName);
    }

    FUNCTION_TEST_RETURN_TYPE(LogPreResult, result);
}

/***********************************************************************************************************************************
Finalize formatting and log after the message has been added to the buffer
***********************************************************************************************************************************/
#define LOG_BANNER                                                  "-------------------PROCESS START-------------------\n"

static void
logPost(LogPreResult *const logData, const LogLevel logLevel, const LogLevel logRangeMin, const LogLevel logRangeMax)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(SIZE, logData->bufferPos);
        FUNCTION_TEST_PARAM(STRINGZ, logData->logBufferStdErr);
        FUNCTION_TEST_PARAM(SIZE, logData->indentSize);
        FUNCTION_TEST_PARAM(ENUM, logLevel);
        FUNCTION_TEST_PARAM(ENUM, logRangeMin);
        FUNCTION_TEST_PARAM(ENUM, logRangeMax);
    FUNCTION_TEST_END();

    ASSERT_LOG_LEVEL(logLevel);
    ASSERT_LOG_LEVEL(logRangeMin);
    ASSERT_LOG_LEVEL(logRangeMax);
    ASSERT(logRangeMin <= logRangeMax);

    // Add linefeed
    logBuffer[logData->bufferPos++] = '\n';
    logBuffer[logData->bufferPos] = 0;

    // Determine where to log the message based on log-level-stderr
    if (logLevel <= logLevelStdErr)
    {
        if ((logLevelStdErr > logLevelStdOut && logRange(logLevelStdErr, logRangeMin, logRangeMax)) ||
            (logLevelStdErr <= logLevelStdOut && logRange(logLevelStdOut, logRangeMin, logRangeMax)))
        {
            logWriteIndent(
                logFdStdErr, logData->logBufferStdErr, logData->indentSize - (size_t)(logData->logBufferStdErr - logBuffer),
                "log to stderr");
        }
    }
    else if (logLevel <= logLevelStdOut && logRange(logLevelStdOut, logRangeMin, logRangeMax))
        logWriteIndent(logFdStdOut, logBuffer, logData->indentSize, "log to stdout");

    // Log to file
    if (logLevel <= logLevelFile && logFdFile != -1 && logRange(logLevelFile, logRangeMin, logRangeMax))
    {
        // If the banner has not been written
        if (!logFileBanner)
        {
            // Add a blank line if the file already has content
            if (lseek(logFdFile, 0, SEEK_END) > 0)
                logWrite(logFdFile, "\n", 1, "banner spacing to file");

            // Write process start banner
            logWrite(logFdFile, LOG_BANNER, sizeof(LOG_BANNER) - 1, "banner to file");

            // Mark banner as written
            logFileBanner = true;
        }

        logWriteIndent(logFdFile, logBuffer, logData->indentSize, "log to file");
    }

    FUNCTION_TEST_RETURN_VOID();
}

/**********************************************************************************************************************************/
#define LOG_SIGNAL_MESSAGE_PRE                                      "terminated on signal "

FN_EXTERN void
logSignal(const LogLevel logLevel, const char *const signalName)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, logLevel);
        FUNCTION_TEST_PARAM(STRINGZ, signalName);
    FUNCTION_TEST_END();

    ASSERT(signalName != NULL);
    STATIC_ASSERT_STMT(LOG_BUFFER_SIZE >= sizeof(LOG_SIGNAL_MESSAGE_PRE), "invalid log buffer size");

    // Initialize log buffer and data with static signal message
    memcpy(logBuffer, LOG_SIGNAL_MESSAGE_PRE, sizeof(LOG_SIGNAL_MESSAGE_PRE) - 1);
    LogPreResult logData = {.bufferPos = sizeof(LOG_SIGNAL_MESSAGE_PRE) - 1, .logBufferStdErr = logBuffer, .indentSize = 4};

    // Add signal name and ensure string is zero-terminated
    strncpy(logBuffer + logData.bufferPos, signalName, sizeof(logBuffer) - logData.bufferPos - 1);
    logData.bufferPos += strlen(signalName);
    logBuffer[sizeof(logBuffer) - 1] = 0;

    logPost(&logData, logLevel, LOG_LEVEL_MIN, LOG_LEVEL_MAX);

    FUNCTION_TEST_RETURN_VOID();
}

/**********************************************************************************************************************************/
FN_EXTERN void
logInternal(
    const LogLevel logLevel, const LogLevel logRangeMin, const LogLevel logRangeMax, const unsigned int processId,
    const char *const fileName, const char *const functionName, const int code, const char *const message)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, logLevel);
        FUNCTION_TEST_PARAM(ENUM, logRangeMin);
        FUNCTION_TEST_PARAM(ENUM, logRangeMax);
        FUNCTION_TEST_PARAM(UINT, processId);
        FUNCTION_TEST_PARAM(STRINGZ, fileName);
        FUNCTION_TEST_PARAM(STRINGZ, functionName);
        FUNCTION_TEST_PARAM(INT, code);
        FUNCTION_TEST_PARAM(STRINGZ, message);
    FUNCTION_TEST_END();

    ASSERT(message != NULL);

    LogPreResult logData = logPre(logLevel, processId, fileName, functionName, code);

    // Copy message into buffer and update buffer position
    strncpy(logBuffer + logData.bufferPos, message, sizeof(logBuffer) - logData.bufferPos);
    logBuffer[sizeof(logBuffer) - 1] = 0;
    logData.bufferPos += strlen(logBuffer + logData.bufferPos);

    logPost(&logData, logLevel, logRangeMin, logRangeMax);

    FUNCTION_TEST_RETURN_VOID();
}

FN_EXTERN void
logInternalFmt(
    const LogLevel logLevel, const LogLevel logRangeMin, const LogLevel logRangeMax, const unsigned int processId,
    const char *const fileName, const char *const functionName, const int code, const char *const format, ...)
{
    FUNCTION_TEST_BEGIN();
        FUNCTION_TEST_PARAM(ENUM, logLevel);
        FUNCTION_TEST_PARAM(ENUM, logRangeMin);
        FUNCTION_TEST_PARAM(ENUM, logRangeMax);
        FUNCTION_TEST_PARAM(UINT, processId);
        FUNCTION_TEST_PARAM(STRINGZ, fileName);
        FUNCTION_TEST_PARAM(STRINGZ, functionName);
        FUNCTION_TEST_PARAM(INT, code);
        FUNCTION_TEST_PARAM(STRINGZ, format);
    FUNCTION_TEST_END();

    ASSERT(format != NULL);

    LogPreResult logData = logPre(logLevel, processId, fileName, functionName, code);

    // Format message into buffer and update buffer position
    va_list argumentList;
    va_start(argumentList, format);
    logData.bufferPos += (size_t)vsnprintf(
        logBuffer + logData.bufferPos, LOG_BUFFER_SIZE - logData.bufferPos, format, argumentList);
    va_end(argumentList);

    logPost(&logData, logLevel, logRangeMin, logRangeMax);

    FUNCTION_TEST_RETURN_VOID();
}
