/*
 *  nextpnr -- Next Generation Place and Route
 *
 *  Copyright (C) 2018  Claire Xenia Wolf <claire@yosyshq.com>
 *
 *  Permission to use, copy, modify, and/or distribute this software for any
 *  purpose with or without fee is hereby granted, provided that the above
 *  copyright notice and this permission notice appear in all copies.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 *  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 *  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 *  ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 *  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 *  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 *  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */

#include <list>
#include <map>
#include <set>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <vector>

#include "log.h"

NEXTPNR_NAMESPACE_BEGIN

NPNR_NORETURN void logv_error(const char *format, va_list ap) NPNR_ATTRIBUTE(noreturn);

std::vector<std::pair<std::ostream *, LogLevel>> log_streams;
log_write_type log_write_function = nullptr;

std::string log_last_error;
void (*log_error_atexit)() = NULL;

dict<LogLevel, int, loglevel_hash_ops> message_count_by_level;
static int log_newline_count = 0;
bool had_nonfatal_error = false;

std::string stringf(const char *fmt, ...)
{
    std::string string;
    va_list ap;

    va_start(ap, fmt);
    string = vstringf(fmt, ap);
    va_end(ap);

    return string;
}

std::string vstringf(const char *fmt, va_list ap)
{
    std::string string;
    char *str = NULL;

#if defined(_WIN32) || defined(__CYGWIN__)
    int sz = 64 + strlen(fmt), rc;
    while (1) {
        va_list apc;
        va_copy(apc, ap);
        str = (char *)realloc(str, sz);
        rc = vsnprintf(str, sz, fmt, apc);
        va_end(apc);
        if (rc >= 0 && rc < sz)
            break;
        sz *= 2;
    }
#else
    if (vasprintf(&str, fmt, ap) < 0)
        str = NULL;
#endif

    if (str != NULL) {
        string = str;
        free(str);
    }

    return string;
}

void logv(const char *format, va_list ap, LogLevel level = LogLevel::LOG_MSG)
{
    //
    // Trim newlines from the beginning
    while (format[0] == '\n' && format[1] != 0) {
        log_always("\n");
        format++;
    }

    std::string str = vstringf(format, ap);

    if (str.empty())
        return;

    size_t nnl_pos = str.find_last_not_of('\n');
    if (nnl_pos == std::string::npos)
        log_newline_count += str.size();
    else
        log_newline_count = str.size() - nnl_pos - 1;

    for (auto f : log_streams)
        if (f.second <= level)
            *f.first << str;
    if (log_write_function)
        log_write_function(str);
}

void log_with_level(LogLevel level, const char *format, ...)
{
    message_count_by_level[level]++;
    va_list ap;
    va_start(ap, format);
    logv(format, ap, level);
    va_end(ap);
}

void logv_prefixed(const char *prefix, const char *format, va_list ap, LogLevel level)
{
    std::string message = vstringf(format, ap);

    log_with_level(level, "%s%s", prefix, message.c_str());
    log_flush();
}

void log_always(const char *format, ...)
{
    va_list ap;
    va_start(ap, format);
    logv(format, ap, LogLevel::ALWAYS_MSG);
    va_end(ap);
}

void log(const char *format, ...)
{
    va_list ap;
    va_start(ap, format);
    logv(format, ap, LogLevel::LOG_MSG);
    va_end(ap);
}

void log_info(const char *format, ...)
{
    va_list ap;
    va_start(ap, format);
    logv_prefixed("Info: ", format, ap, LogLevel::INFO_MSG);
    va_end(ap);
}

void log_warning(const char *format, ...)
{
    va_list ap;
    va_start(ap, format);
    logv_prefixed("Warning: ", format, ap, LogLevel::WARNING_MSG);
    va_end(ap);
}

void log_error(const char *format, ...)
{
    va_list ap;
    va_start(ap, format);
    logv_prefixed("ERROR: ", format, ap, LogLevel::ERROR_MSG);

    if (log_error_atexit)
        log_error_atexit();

    throw log_execution_error_exception();
}

void log_break()
{
    if (log_newline_count < 2)
        log("\n");
    if (log_newline_count < 2)
        log("\n");
}

void log_nonfatal_error(const char *format, ...)
{
    va_list ap;
    va_start(ap, format);
    logv_prefixed("ERROR: ", format, ap, LogLevel::ERROR_MSG);
    va_end(ap);
    had_nonfatal_error = true;
}

void log_flush()
{
    for (auto f : log_streams)
        f.first->flush();
}

NEXTPNR_NAMESPACE_END