/* **************************************************************************
 *            qoflog.c
 *
 *  Mon Nov 21 14:41:59 2005
 *  Author: Rob Clark (rclark@cs.hmc.edu)
 *  Copyright (C) 1997-2003 Linas Vepstas <linas@linas.org>
 *  Copyright  2005  Neil Williams <linux@codehelp.co.uk>
 *  Copyright 2007 Joshua Sled <jsled@asynchronous.org>
 *************************************************************************** */

/*
 *  This program is free software; you can 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 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 *  02110-1301,  USA
 */
#include <glib.h>
#include <glib/gstdio.h>

#include <config.h>

#include <platform.h>
#if PLATFORM(WINDOWS)
#include <windows.h>
#endif

#ifdef HAVE_UNISTD_H
# include <unistd.h>
#else
# ifdef __GNUC__
#  warning "<unistd.h> required."
# endif
#endif
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "qof.log"
#include "qof.h"
#include "qoflog.h"
#include <string>
#include <string_view>
#include <vector>
#include <memory>
#include <algorithm>

#define QOF_LOG_MAX_CHARS 50
#define QOF_LOG_MAX_CHARS_WITH_ALLOWANCE 100
#define QOF_LOG_INDENT_WIDTH 4
#define NUM_CLOCKS 10

static FILE *fout = NULL;
static gchar* function_buffer = NULL;
static gint qof_log_num_spaces = 0;
static GLogFunc previous_handler = NULL;
static gchar* qof_logger_format = NULL;
static QofLogModule log_module = "qof";

using StrVec = std::vector<std::string>;

struct ModuleEntry;
using ModuleEntryPtr = std::unique_ptr<ModuleEntry>;
using MEVec = std::vector<ModuleEntryPtr>;

static constexpr int parts = 4; //Log domain parts vector preallocation size
static constexpr QofLogLevel default_level = QOF_LOG_WARNING;
static QofLogLevel current_max{default_level};

struct ModuleEntry
{
    ModuleEntry(const std::string& name, QofLogLevel level) :
        m_name{name}, m_level{level} {
            m_children.reserve(parts);
        }
    ~ModuleEntry() = default;
    std::string m_name;
    QofLogLevel m_level;
    MEVec m_children;
};

static ModuleEntryPtr _modules = NULL;

static ModuleEntry*
get_modules()
{
    if (!_modules)
        _modules = std::make_unique<ModuleEntry>("", default_level);
    return _modules.get();
}

static StrVec
split_domain (const std::string_view domain)
{
    StrVec domain_parts;
    domain_parts.reserve(parts);
    int start = 0;
    auto pos = domain.find(".");
    if (pos == std::string_view::npos)
    {
        domain_parts.emplace_back(domain);
    }
    else
    {
        while (pos != std::string_view::npos)
        {
            auto part_name{domain.substr(start, pos - start)};
            domain_parts.emplace_back(part_name);
            start = pos + 1;
            pos = domain.find(".", start);
        }
        auto part_name{domain.substr(start, pos)};
        domain_parts.emplace_back(part_name);
    }
    return domain_parts;
}

void
qof_log_indent(void)
{
    qof_log_num_spaces += QOF_LOG_INDENT_WIDTH;
}

void
qof_log_dedent(void)
{
    qof_log_num_spaces
    = (qof_log_num_spaces < QOF_LOG_INDENT_WIDTH)
      ? 0
      : qof_log_num_spaces - QOF_LOG_INDENT_WIDTH;
}

void
qof_log_set_file(FILE *outfile)
{
    if (!outfile)
    {
        fout = stderr;
        return;
    }
    fout = outfile;
}

void
qof_log_init(void)
{
    qof_log_init_filename(NULL);
}

static void
log4glib_handler(const gchar     *log_domain,
                 GLogLevelFlags  log_level,
                 const gchar     *message,
                 gpointer        user_data)
{
    QofLogLevel level = static_cast<QofLogLevel>(log_level);
    if (G_LIKELY(!qof_log_check(log_domain, level)))
        return;

    {
        char timestamp_buf[10];
        time64 now;
        struct tm now_tm;
        const char *format_24hour =
#ifdef G_OS_WIN32
            "%H:%M:%S"
#else
            "%T"
#endif
            ;
        const char *level_str = qof_log_level_to_string(level);
        now = gnc_time (NULL);
        gnc_localtime_r (&now, &now_tm);
        qof_strftime(timestamp_buf, 9, format_24hour, &now_tm);

        fprintf(fout, qof_logger_format,
                timestamp_buf,
                5, level_str,
                (log_domain == NULL ? "" : log_domain),
                qof_log_num_spaces, "",
                message,
                (g_str_has_suffix(message, "\n") ? "" : "\n"));
        fflush(fout);
    }

    /* chain?  ignore?  Only chain if it's going to be quiet...
    else
    {
         // chain
         previous_handler(log_domain, log_level, message, NULL);
    }
    */
}

void
qof_log_init_filename(const gchar* log_filename)
{
    gboolean warn_about_missing_permission = FALSE;
    auto modules = get_modules();

    if (!qof_logger_format)
        qof_logger_format = g_strdup ("* %s %*s <%s> %*s%s%s"); //default format

    if (log_filename)
    {
        int fd;
        gchar *fname;

        if (fout != NULL && fout != stderr && fout != stdout)
            fclose(fout);

        fname = g_strconcat(log_filename, ".XXXXXX.log", nullptr);

        if ((fd = g_mkstemp(fname)) != -1)
        {
#if PLATFORM(WINDOWS)
            /* MSVC compiler: Somehow the OS thinks file descriptor from above
             * still isn't open. So we open normally with the file name and that's it. */
            fout = g_fopen(fname, "wb");
#else
            /* We must not overwrite /dev/null */
            g_assert(g_strcmp0(log_filename, "/dev/null") != 0);

            /* Windows prevents renaming of open files, so the next command silently fails there
             * No problem, the filename on Windows will simply have the random characters */
            g_rename(fname, log_filename);
            fout = fdopen(fd, "w");
#endif
            if (!fout)
                warn_about_missing_permission = TRUE;
        }
        else
        {
            warn_about_missing_permission = TRUE;
            fout = stderr;
        }
        g_free(fname);
    }

    if (!fout)
        fout = stderr;

    if (previous_handler == NULL)
        previous_handler = g_log_set_default_handler(log4glib_handler, modules);

    if (warn_about_missing_permission)
    {
        g_critical("Cannot open log output file \"%s\", using stderr.", log_filename);
    }
}

void
qof_log_shutdown (void)
{
    if (fout && fout != stderr && fout != stdout)
    {
        fclose(fout);
        fout = NULL;
    }

    if (function_buffer)
    {
        g_free(function_buffer);
        function_buffer = NULL;
    }

    if (_modules != NULL)
    {
        _modules = nullptr;
    }

    if (previous_handler != NULL)
    {
        g_log_set_default_handler(previous_handler, NULL);
        previous_handler = NULL;
    }
}

void
qof_log_set_level(QofLogModule log_module, QofLogLevel level)
{
    if (!log_module || level == QOF_LOG_FATAL)
        return;

    if (level > current_max)
        current_max = level;

    auto module_parts = split_domain(log_module);
    auto module = get_modules();
    for (auto part : module_parts)
    {
        auto iter = std::find_if(module->m_children.begin(),
                              module->m_children.end(),
                              [part](auto& child){
                                  return child && part == child->m_name;
                              });
        if (iter == module->m_children.end())
        {
            auto child = std::make_unique<ModuleEntry>(part, default_level);
            module->m_children.emplace_back(std::move(child));
            module = module->m_children.back().get();
        }
        else
        {
            module = iter->get();
        }
    }
    module->m_level = level;
}


gboolean
qof_log_check(QofLogModule domain, QofLogLevel level)
{
// Check the global levels
    if (level > current_max)
        return FALSE;
    if (level <= default_level)
        return TRUE;
    auto module = get_modules();
    // If the level <= the default then no need to look further.
    if (level <= module->m_level)
        return TRUE;

    if (!domain)
        return FALSE;

    auto domain_vec = split_domain(domain);

    for (const auto& part : domain_vec)
    {
        auto iter = std::find_if(module->m_children.begin(),
                               module->m_children.end(),
                               [part](auto& child) {
                                   return child && part == child->m_name; });

        if (iter == module->m_children.end())
            return FALSE;

        if (level <= (*iter)->m_level)
            return TRUE;

        module = iter->get();
    }
    return FALSE;
}

const char *
qof_log_prettify (const char *name)
{
    gchar *p, *buffer, *begin;
    gint length;

    if (!name)
    {
        return "";
    }
/* Clang's __func__ displays the whole signature, like a good C++
 * compier should. Gcc displays only the name of the function. Strip
 * the extras from Clang's output so that log messages are the same
 * regardless of compiler.
 */
    buffer = g_strndup(name, QOF_LOG_MAX_CHARS_WITH_ALLOWANCE - 1);
    length = strlen(buffer);
    p = g_strstr_len (buffer, length, "(");
    if (p) *p = '\0';
    begin = g_strrstr (buffer, "*");
    if (begin == NULL)
        begin = g_strrstr (buffer, " ");
    else if (* (begin + 1) == ' ')
        ++ begin;
    if (begin != NULL)
        p = begin + 1;
    else
        p = buffer;

    if (function_buffer)
        g_free(function_buffer);
    function_buffer = g_strdup(p);
    g_free(buffer);
    return function_buffer;
}

void
qof_log_init_filename_special(const char *log_to_filename)
{
    if (g_ascii_strcasecmp("stderr", log_to_filename) == 0)
    {
        qof_log_init();
        qof_log_set_file(stderr);
    }
    else if (g_ascii_strcasecmp("stdout", log_to_filename) == 0)
    {
        qof_log_init();
        qof_log_set_file(stdout);
    }
    else
    {
        qof_log_init_filename(log_to_filename);
    }
}

void
qof_log_parse_log_config(const char *filename)
{
    const gchar *levels_group = "levels", *output_group = "output";
    GError *err = NULL;
    GKeyFile *conf = g_key_file_new();

    if (!g_key_file_load_from_file(conf, filename, G_KEY_FILE_NONE, &err))
    {
        g_warning("unable to parse [%s]: %s", filename, err->message);
        g_error_free(err);
        return;
    }

    DEBUG("parsing log config from [%s]", filename);
    if (g_key_file_has_group(conf, levels_group))
    {
        gsize num_levels;
        unsigned int key_idx;
        gchar **levels;
        gint logger_max_name_length = 12;
        gchar *str = NULL;

        levels = g_key_file_get_keys(conf, levels_group, &num_levels, NULL);

        for (key_idx = 0; key_idx < num_levels && levels[key_idx] != NULL; key_idx++)
        {
            QofLogLevel level;
            gchar *logger_name = NULL, *level_str = NULL;

            logger_name = g_strdup(levels[key_idx]);
            logger_max_name_length = MAX (logger_max_name_length, (gint) strlen (logger_name));
            level_str = g_key_file_get_string(conf, levels_group, logger_name, NULL);
            level = qof_log_level_from_string(level_str);

            DEBUG("setting log [%s] to level [%s=%d]", logger_name, level_str, level);
            qof_log_set_level(logger_name, level);

            g_free(logger_name);
            g_free(level_str);
        }

        str = g_strdup_printf ("%d", logger_max_name_length);
        if (qof_logger_format)
            g_free (qof_logger_format);
        qof_logger_format = g_strconcat ("* %s %*s <%-", str, ".", str, "s> %*s%s%s", nullptr);

        g_free (str);
        g_strfreev(levels);
    }

    if (g_key_file_has_group(conf, output_group))
    {
        gsize num_outputs;
        unsigned int output_idx;
        gchar **outputs;

        outputs = g_key_file_get_keys(conf, output_group, &num_outputs, NULL);
        for (output_idx = 0; output_idx < num_outputs && outputs[output_idx] != NULL; output_idx++)
        {
            gchar *key = outputs[output_idx];
            gchar *value;

            if (g_ascii_strcasecmp("to", key) != 0)
            {
                g_warning("unknown key [%s] in [outputs], skipping", key);
                continue;
            }

            value = g_key_file_get_string(conf, output_group, key, NULL);
            DEBUG("setting [output].to=[%s]", value);
            qof_log_init_filename_special(value);
            g_free(value);
        }
        g_strfreev(outputs);
    }

    g_key_file_free(conf);
}

const gchar*
qof_log_level_to_string(QofLogLevel log_level)
{
    const char *level_str;
    switch (log_level)
    {
    case QOF_LOG_FATAL:
        level_str = "FATAL";
        break;
    case QOF_LOG_ERROR:
        level_str = "ERROR";
        break;
    case QOF_LOG_WARNING:
        level_str = "WARN";
        break;
    case QOF_LOG_MESSAGE:
        level_str = "MESSG";
        break;
    case QOF_LOG_INFO:
        level_str = "INFO";
        break;
    case QOF_LOG_DEBUG:
        level_str = "DEBUG";
        break;
    default:
        level_str = "OTHER";
        break;
    }
    return level_str;
}

QofLogLevel
qof_log_level_from_string(const gchar *str)
{
    if (g_ascii_strncasecmp("error", str, 5) == 0) return QOF_LOG_FATAL;
    if (g_ascii_strncasecmp("crit", str, 4) == 0) return QOF_LOG_ERROR;
    if (g_ascii_strncasecmp("warn", str, 4) == 0) return QOF_LOG_WARNING;
    if (g_ascii_strncasecmp("mess", str, 4) == 0) return QOF_LOG_MESSAGE;
    if (g_ascii_strncasecmp("info", str, 4) == 0) return QOF_LOG_INFO;
    if (g_ascii_strncasecmp("debug", str, 5) == 0) return QOF_LOG_DEBUG;
    return QOF_LOG_DEBUG;
}
