/*
 * Copyright (C) 2009 Patrick Ohly <patrick.ohly@gmx.de>
 * Copyright (C) 2009 Intel Corporation
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) version 3.
 *
 * This library 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.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301  USA
 */

#include <syncevo/Logging.h>
#include <syncevo/LogStdout.h>
#include <syncevo/LogRedirect.h>

#include <vector>
#include <string.h>

#include <syncevo/declarations.h>
SE_BEGIN_CXX

static RecMutex logMutex;
/**
 * POD to have it initialized without relying on a constructor to run.
 */
static std::string *logProcessName;

void Logger::setProcessName(const std::string &name)
{
    RecMutex::Guard guard = logMutex.lock();
    if (!logProcessName) {
        logProcessName = new std::string(name);
    } else {
        *logProcessName = name;
    }
}

std::string Logger::getProcessName()
{
    RecMutex::Guard guard = logMutex.lock();
    return logProcessName ? *logProcessName : "";
}

RecMutex::Guard Logger::lock()
{
    return logMutex.lock();
}

Logger::Logger() :
    m_level(INFO)
{
}

Logger::~Logger()
{
}

/**
 * Create (if necessary) and return the logger stack.
 * It has at least one entry.
 *
 * logMutex must be locked when calling this.
 */
static std::vector<Logger::Handle> &LoggersSingleton()
{
    // allocate array once and never free it because it might be needed till
    // the very end of the application life cycle
    static std::vector<Logger::Handle> *loggers;
    if (!loggers) {
        loggers = new std::vector<Logger::Handle>;
        // Ensure that the array is never empty.
        auto logger = std::make_shared<LoggerStdout>();
        loggers->push_back(logger);
    }
    return *loggers;
}

Logger::Handle Logger::instance()
{
    RecMutex::Guard guard = logMutex.lock();
    std::vector<Handle> &loggers = LoggersSingleton();
    return loggers.back();
}

void Logger::addLogger(const Handle &logger)
{
    RecMutex::Guard guard = logMutex.lock();
    std::vector<Handle> &loggers = LoggersSingleton();

    loggers.push_back(logger);
}

void Logger::removeLogger(Logger *logger)
{
    RecMutex::Guard guard = logMutex.lock();
    std::vector<Handle> &loggers = LoggersSingleton();

    for (ssize_t i = loggers.size() - 1;
         i >= 0;
         --i) {
        if (loggers[i] == logger) {
            loggers[i].remove();
            loggers.erase(loggers.begin() + i);
            break;
        }
    }
}

void Logger::formatLines(Level msglevel,
                             Level outputlevel,
                             const std::string *processName,
                             const std::string *prefix,
                             const char *format,
                             va_list args,
                             std::function<void (std::string &buffer, size_t expectedTotal)> print)
{
    std::string tag;

    // in case of 'SHOW' level, don't print level and prefix information
    if (msglevel != SHOW) {
        std::string reltime;
        std::string procname;
        std::string firstLine;

        // Run non-blocking operations on shared data while
        // holding the mutex.
        {
            RecMutex::Guard guard = logMutex.lock();
            const std::string *realProcname;

            if (processName) {
                realProcname = processName;
            } else {
                if (!logProcessName) {
                    logProcessName = new std::string;
                }
                realProcname = logProcessName;
            }
            if (!realProcname->empty()) {
                procname.reserve(realProcname->size() + 1);
                procname += " ";
                procname += *realProcname;
            }

            if (outputlevel >= DEBUG) {
                // add relative time stamp
                Timespec now = Timespec::monotonic();
                if (!m_startTime) {
                    // first message, start counting time
                    m_startTime = now;
                    time_t nowt = time(nullptr);
                    struct tm tm_gm, tm_local;
                    char buffer[2][80];
                    gmtime_r(&nowt, &tm_gm);
                    localtime_r(&nowt, &tm_local);
                    reltime = " 00:00:00";
                    strftime(buffer[0], sizeof(buffer[0]),
                             "%a %Y-%m-%d %H:%M:%S",
                             &tm_gm);
                    strftime(buffer[1], sizeof(buffer[1]),
                             "%H:%M %z %Z",
                             &tm_local);
                    std::string line =
                        StringPrintf("[DEBUG%s%s] %s UTC = %s\n",
                                     procname.c_str(),
                                     reltime.c_str(),
                                     buffer[0],
                                     buffer[1]);
                } else {
                    if (now >= m_startTime) {
                        Timespec delta = now - m_startTime;
                        reltime = StringPrintf(" %02ld:%02ld:%02ld",
                                               delta.tv_sec / (60 * 60),
                                               (delta.tv_sec % (60 * 60)) / 60,
                                               delta.tv_sec % 60);
                    } else {
                        reltime = " ??:??:??";
                    }
                }
            }
        }

        if (!firstLine.empty()) {
            print(firstLine, 1);
        }
        if (prefix && prefix->empty()) {
            prefix = nullptr;
        }
        tag = StringPrintf("[%s%s%s] %s%s",
                           levelToStr(msglevel),
                           procname.c_str(),
                           reltime.c_str(),
                           prefix ? prefix->c_str() : "",
                           prefix ? ": " : "");
    }

    std::string output = StringPrintfV(format, args);

    if (!tag.empty()) {
        // Print individual lines.
        //
        // Total size is guessed by assuming an average line length of
        // around 40 characters to predict number of lines.
        size_t expectedTotal = (output.size() / 40 + 1) * tag.size() + output.size();
        size_t pos = 0;
        while (true) {
            size_t next = output.find('\n', pos);
            if (next != output.npos) {
                std::string line;
                line.reserve(tag.size() + next + 1 - pos);
                line.append(tag);
                line.append(output, pos, next + 1 - pos);
                print(line, expectedTotal);
                pos = next + 1;
            } else {
                break;
            }
        }
        if (pos < output.size() || output.empty()) {
            // handle dangling last line or empty chunk (don't
            // want empty line for that, print at least the tag)
            std::string line;
            line.reserve(tag.size() + output.size() - pos + 1);
            line.append(tag);
            line.append(output, pos, output.size() - pos);
            line += '\n';
            print(line, expectedTotal);
        }
    } else {
        if (!boost::ends_with(output, "\n")) {
            // append newline if necessary
            output += '\n';
        }
        print(output, 0);
    }
}

Logger::MessageOptions::MessageOptions(Level level) :
    m_level(level),
    m_prefix(nullptr),
    m_file(nullptr),
    m_line(0),
    m_function(nullptr),
    m_processName(nullptr),
    m_flags(0)
{
}

Logger::MessageOptions::MessageOptions(Level level,
                                       const std::string *prefix,
                                       const char *file,
                                       int line,
                                       const char *function,
                                       int flags) :
    m_level(level),
    m_prefix((!prefix || prefix->empty()) ? nullptr : prefix),
    m_file(file),
    m_line(line),
    m_function(function),
    m_processName(nullptr),
    m_flags(flags)
{
}

Logger::Handle::Handle() throw ()
{
}

Logger::Handle::Handle(Logger *logger) throw ()
{
    m_logger.reset(logger);
}

Logger::Handle::Handle(const Handle &other) throw ()
{
    m_logger = other.m_logger;
}

Logger::Handle & Logger::Handle::operator = (const Handle &other) throw ()
{
    if (this != &other) {
        m_logger = other.m_logger;
    }
    return *this;
}

Logger::Handle::~Handle() throw ()
{
    m_logger.reset();
}

void Logger::Handle::message(Level level,
                             const std::string *prefix,
                             const char *file,
                             int line,
                             const char *function,
                             const char *format,
                             ...)
{
    va_list args;
    va_start(args, format);
    m_logger->messagev(MessageOptions(level, prefix, file, line, function), format, args);
    va_end(args);
}

void Logger::Handle::message(Level level,
                             const std::string &prefix,
                             const char *file,
                             int line,
                             const char *function,
                             const char *format,
                             ...)
{
    va_list args;
    va_start(args, format);
    m_logger->messagev(MessageOptions(level, &prefix, file, line, function), format, args);
    va_end(args);
}

void Logger::Handle::messageWithOptions(const MessageOptions &options,
                                        const char *format,
                                        ...)
{
    va_list args;
    va_start(args, format);
    m_logger->messagev(options, format, args);
    va_end(args);
}

const char *Logger::levelToStr(Level level)
{
    switch (level) {
    case SHOW: return "SHOW";
    case ERROR: return "ERROR";
    case WARNING: return "WARNING";
    case INFO: return "INFO";
    case DEV: return "DEVELOPER";
    case DEBUG: return "DEBUG";
    default: return "???";
    }
}

Logger::Level Logger::strToLevel(const char *str)
{
    // order is based on a rough estimate of message frequency of the
    // corresponding type
    if (!str || !strcmp(str, "DEBUG")) {
        return DEBUG;
    } else if (!strcmp(str, "INFO")) {
        return INFO;
    } else if (!strcmp(str, "SHOW")) {
        return SHOW;
    } else if (!strcmp(str, "ERROR")) {
        return ERROR;
    } else if (!strcmp(str, "WARNING")) {
        return WARNING;
    } else if (!strcmp(str, "DEV")) {
        return DEV;
    } else {
        return DEBUG;
    }
}

#ifdef HAVE_GLIB
void Logger::glogFunc(const gchar *logDomain,
                      GLogLevelFlags logLevel,
                      const gchar *message,
                      gpointer userData)
{
    Level level =
        (logLevel & (G_LOG_LEVEL_ERROR|G_LOG_LEVEL_CRITICAL)) ? ERROR :
        // glib warnings are usually not relevant for users, only for developers.
        (logLevel & G_LOG_LEVEL_WARNING) ? DEV :
        (logLevel & (G_LOG_LEVEL_MESSAGE|G_LOG_LEVEL_INFO)) ? SHOW :
        DEBUG;

    // Downgrade some know error messages as registered with
    // the LogRedirect helper class. That messages are registered
    // there is a historic artifact.
    if (level != DEBUG &&
        LogRedirect::ignoreError(message)) {
        level = DEBUG;
    }

    Logger::instance().message(level,
                               nullptr,
                               nullptr,
                               0,
                               nullptr,
                               "%s%s%s",
                               logDomain ? logDomain : "",
                               logDomain ? ": " : "",
                               message);
}

#endif

int Logger::sysyncPrintf(FILE *stream,
                         const char *format,
                         ...)
{
    va_list args;
    va_start(args, format);
    static const std::string prefix("SYSYNC");
    if (boost::starts_with(format, prefix) &&
        format[prefix.size()] == ' ') {
        // Skip initial "SYSYNC " prefix, because it will get re-added
        // in a better way (= to each line) via the prefix parameter.
        format += prefix.size() + 1;
    }
    Logger::instance().messagev(MessageOptions(DEBUG, &prefix, nullptr, 0, nullptr), format, args);
    va_end(args);

    return 0;
}

SE_END_CXX
