#include "tile/base/internal/logging.h"
#include <algorithm>
#include <cstdio>
#include <iostream>
#include <map>
#include <mutex>
#include <set>

#ifdef NDEBUG
#undef NDEBUG
#endif
#include <assert.h>

namespace tile {
namespace internal {
namespace logging {

static const char *LOG_CONST_TABLE[][3] = {
    {"\033[46;37m", "\033[36m", "I"},
    {"\033[43;37m", "\033[33m", "W"},
    {"\033[41;37m", "\033[31m", "E"},
    {"\033[44;37m", "\033[34m", "T"},
    {"\033[42;37m", "\033[32m", "D"},
};

namespace {
std::vector<PrefixAppender *> *
GetProviers()
{
    static std::vector<PrefixAppender *> providers;
    return &providers;
}
}// namespace

namespace details {
std::string
DescribeFormatArguments(const std::vector<std::string> &args)
{
    return fmt::format("{}", fmt::join(args.begin(), args.end(), ", "));
}
}// namespace details

void
InstallPrefixProvider(PrefixAppender *writer)
{
    GetProviers()->push_back(writer);
}

void
WritePrefixTo(std::string *to)
{
    for (auto &&appender : *GetProviers()) {
        auto was = to->size();
        appender(to);
        if (to->size() != was) { to->push_back(' '); }
    }
}
}// namespace logging
}// namespace internal
}// namespace tile

namespace tile {
static std::mutex g_sink_mutex;
static std::set<LogSink::Ptr> g_sinks;
static std::vector<LogSink::Ptr> g_sinks_arr;
static std::atomic<int> g_sink_count{0};
static std::atomic<bool> g_use_localtime{true};

static uint64_t g_num_messages[TILE_MAX_LOG_SEVERITY + 1];
std::mutex g_log_mutex;

class OStreamWrapper : public std::streambuf {
public:
    OStreamWrapper(std::ostream &ostream) : ostream_(ostream) {}

    int_type overflow(int_type c) override { return c; }

private:
    std::ostream &ostream_;
};

bool
VLogIsOn(int n)
{
    return true;
}

class LogMessage::Impl {
public:
    Impl(const char *file, int line) : stream_(message_text_, LogMessage::kMaxLogMessageLen, 0)
    {
        Init(file, line, TILE_INFO, &LogMessage::Impl::SendToLog);
    }

    Impl(const char *file, int line, LogSeverity severity) : stream_(message_text_, LogMessage::kMaxLogMessageLen, 0)
    {
        Init(file, line, severity, &LogMessage::Impl::SendToLog);
    }

    Impl(const char *file, int line, LogSeverity severity, LogSink *sink, bool also_send_to_log)
        : stream_(message_text_, LogMessage::kMaxLogMessageLen, 0)
    {
        Init(file, line, severity,
             also_send_to_log ? &LogMessage::Impl::SendToSinkAndLog : &LogMessage::Impl::SendToSink);
        sink_ = sink;
    }

    ~Impl() { Flush(); }

    std::ostream &stream() { return stream_; }

    void Flush()
    {
        if (has_been_flushed_) { return; }

        num_chars_to_log_ = stream_.pcount();
        if (num_chars_to_log_ == 0) {
            message_text_[0] = '\n';
            ++num_chars_to_log_;
        }

        const bool append_newline = message_text_[num_chars_to_log_ - 1] != '\n';
        char original_final_char  = '\0';

        if (append_newline) {
            original_final_char                = message_text_[num_chars_to_log_];
            message_text_[num_chars_to_log_++] = '\n';
        }
        message_text_[num_chars_to_log_] = '\0';

        {
            std::lock_guard<std::mutex> _(g_log_mutex);
            (this->*(send_method_))();
            ++g_num_messages[severity_];
            WaitForSinks();
            if (sink_) {
                try {
                    sink_->Flush();
                } catch (const std::exception &e) {
                    fprintf(stderr, "\nLogSink[name=%s], reason=%s\n", sink_->name(), e.what());
                    assert(false);
                }
            }
        }

        if (append_newline) { message_text_[num_chars_to_log_ - 1] = original_final_char; }

        if (preserved_errno_ != 0) { errno = preserved_errno_; }
        has_been_flushed_ = true;

        if (severity_ == TILE_FATAL) { assert(false && "FATAL log"); }
    }

    LogSeverity severity() const noexcept { return severity_; }

    int line() const noexcept { return line_; }

    const char *fullname() const noexcept { return file_; }

    const char *basename() const noexcept { return file_; }

    const LogMessageTime &time() const noexcept { return time_; }

    int preserved_errno() const { return preserved_errno_; }

private:
    void Init(const char *file, int line, LogSeverity severity, void (LogMessage::Impl::*send_method)())
    {
        has_been_flushed_ = false;
        file_             = file;
        line_             = line;
        severity_         = severity;
        send_method_      = send_method;
        sink_             = nullptr;
        const auto now    = std::chrono::system_clock::now();
        time_             = LogMessageTime(now);
        thread_id_        = std::this_thread::get_id();
        preserved_errno_  = errno;

        num_chars_to_log_ = 0;
        num_prefix_chars_ = 0;
    }

    void SendToSink()
    {
        if (sink_) {
            sink_->Send(severity_, fullname(), basename(), line(), time(), message_text_ + num_prefix_chars_,
                        num_chars_to_log_ - num_prefix_chars_ - 1);
        }
    }

    void SendToLog()
    {
        if (g_sink_count.load(std::memory_order_relaxed) == 0) {
            ColoredWriteToStdout(severity_, message_text_ + num_prefix_chars_, num_chars_to_log_ - num_chars_to_log_);
        }
        // HACK: temp fix
        auto msg_len = num_chars_to_log_ - num_prefix_chars_ - 1;
        msg_len      = msg_len > (LogMessage::kMaxLogMessageLen + 1) ? 0 : msg_len;
        LogToSinks(severity_, fullname(), basename(), line(), time(), message_text_ + num_prefix_chars_, msg_len);
    }

    void SendToSinkAndLog()
    {
        SendToLog();
        SendToSink();
    }

private:
    char message_text_[LogMessage::kMaxLogMessageLen + 1];
    LogMessage::LogStream stream_;

    bool has_been_flushed_;

    const char *file_;
    int line_;
    LogSeverity severity_;
    void (LogMessage::Impl::*send_method_)();

    LogSink *sink_;
    LogMessageTime time_;
    std::thread::id thread_id_;
    int preserved_errno_;

    size_t num_prefix_chars_;
    size_t num_chars_to_log_;
};

LogMessage::LogMessage(const char *file, int line) : impl_(new Impl(file, line)) {}

LogMessage::LogMessage(const char *file, int line, LogSeverity severity) : impl_(new Impl(file, line, severity)) {}

LogMessage::LogMessage(const char *file, int line, LogSeverity severity, LogSink *sink, bool also_send_to_log)
    : impl_(new Impl(file, line, severity, sink, also_send_to_log))
{}

LogMessage::~LogMessage() {}

std::ostream &
LogMessage::stream()
{
    return impl_->stream();
}

void
LogMessage::Flush()
{
    impl_->Flush();
}

LogSeverity
LogMessage::severity() const noexcept
{
    return impl_->severity();
}

int
LogMessage::line() const noexcept
{
    return impl_->line();
}

const char *
LogMessage::fullname() const noexcept
{
    return impl_->fullname();
}

const char *
LogMessage::basename() const noexcept
{
    return impl_->basename();
}

const LogMessageTime &
LogMessage::time() const noexcept
{
    return impl_->time();
}

int
LogMessage::preserved_errno() const
{
    return impl_->preserved_errno();
}

LogMessageFatal::LogMessageFatal(const char *file, int line) : LogMessage(file, line, TILE_FATAL) {}

LogMessageFatal::~LogMessageFatal() {}

std::ostream &
LogMessageFatal::stream()
{
    return LogMessage::stream();
}

LogMessageTime::LogMessageTime() : time_struct_(), timestamp_(0), usecs_(0), gmtoffset_(0) {}

LogMessageTime::LogMessageTime(std::tm t) : use_localtime_(g_use_localtime.load(std::memory_order_relaxed))
{
    std::time_t timestamp = std::mktime(&t);
    init(t, timestamp, 0);
}

LogMessageTime::LogMessageTime(std::time_t timestamp, WallTime now)
    : use_localtime_(g_use_localtime.load(std::memory_order_relaxed))
{
    std::tm t;
    if (use_localtime_) {
        localtime_r(&timestamp, &t);
    } else {
        gmtime_r(&timestamp, &t);
    }

    init(t, timestamp, now);
}

LogMessageTime::LogMessageTime(std::chrono::system_clock::time_point tp)
    : use_localtime_(g_use_localtime.load(std::memory_order_relaxed))
{
    std::time_t timestamp = std::chrono::system_clock::to_time_t(tp);
    std::tm t;
    if (use_localtime_) {
        localtime_r(&timestamp, &t);
    } else {
        gmtime_r(&timestamp, &t);
    }

    init(t, timestamp, 0);
    usecs_ = std::chrono::duration_cast<std::chrono::microseconds>(tp.time_since_epoch()).count() % 1000000;
}

void
LogMessageTime::init(const std::tm &t, std::time_t timestamp, WallTime now)
{
    time_struct_ = t;
    timestamp_   = timestamp;
    if (now < timestamp) {
        usecs_ = 0;
    } else {
        usecs_ = static_cast<std::int32_t>((now - timestamp) * 1000000) % 1000000;
    }

    CalcGmtOffset();
}

void
LogMessageTime::CalcGmtOffset()
{
    std::tm gmt_struct;
    int isDst = 0;
    if (true) {
        localtime_r(&timestamp_, &gmt_struct);
        isDst      = gmt_struct.tm_isdst;
        gmt_struct = time_struct_;
    } else {
        isDst = time_struct_.tm_isdst;
        gmtime_r(&timestamp_, &gmt_struct);
    }

    time_t gmt_sec       = mktime(&gmt_struct);
    const long hour_secs = 3600;
    // If the Daylight Saving Time(isDst) is active subtract an hour from the
    // current timestamp.
    gmtoffset_ = static_cast<long int>(timestamp_ - gmt_sec + (isDst ? hour_secs : 0));
}

LogSink::~LogSink() {}

bool
LogSink::ShouldLog(LogSeverity severity)
{
    return true;
}

void
LogSink::Send(LogSeverity severity,
              const char *full_filename,
              const char *base_filename,
              int line,
              const LogMessageTime &logmsgtime,
              const char *message,
              size_t message_len)
{

    // do nothing
}

void
LogSink::Flush()
{}

std::string
LogSink::ToString(LogSeverity severity,
                  const char *file,
                  const char *base_filename,
                  int line,
                  const LogMessageTime &logmsgtime,
                  const char *message,
                  size_t message_len)
{
    std::stringstream ss;
    char date_time[64];
    // sprintf(date_time, "%4d-%02d-%02dT%02d:%02d:%02d.%06d", logmsgtime.year() + 1900, logmsgtime.month() + 1,
    //         logmsgtime.day(), logmsgtime.hour(), logmsgtime.min(), logmsgtime.sec(), logmsgtime.usec());
    sprintf(date_time, "%4d-%02d-%02dT%02d:%02d:%02d.%06d", logmsgtime.year() + 1900, logmsgtime.month() + 1,
            logmsgtime.day(), logmsgtime.hour(), logmsgtime.min(), logmsgtime.sec(), logmsgtime.usec());

    ss << date_time;

    if (logmsgtime.gmtoff() != 0) {
        auto hour = logmsgtime.gmtoff() / 3600;
        auto min  = std::abs(logmsgtime.gmtoff() % 3600) / 60;
        sprintf(date_time, "%+03ld:%02ld", hour, min);
        ss << date_time;
    } else if (!logmsgtime.use_localtime()) {
        ss << "Z";
    }
    ss << " " << GetLogSeverityName(severity)[0] << " ";
    ss << base_filename << ":" << line << " ";

    // return google::LogSink::ToString(severity, file, line, logmsgtime.tm(),
    //                                  message, message_len);

    ss.write(message, message_len);
    return ss.str();
}

static void
ColoredWriteToStderrOrStdout(FILE *output, LogSeverity severity, const char *message, size_t len)
{
    char buf[20];
    {
        int n = snprintf(buf, sizeof(buf), "%s", internal::logging::LOG_CONST_TABLE[severity][1]);
        fwrite(buf, n, 1, output);
    }
    fwrite(message, len, 1, output);
    fflush(output);
}

void
LogUseLocaltime(bool use_localtime)
{
    g_use_localtime.store(use_localtime, std::memory_order_relaxed);
}

void
ColoredWriteToStdout(LogSeverity severity, const char *message, size_t len)
{
    FILE *output = severity < TILE_FATAL ? stdout : stderr;
    ColoredWriteToStderrOrStdout(output, severity, message, len);
}

void
ColoredWriteToStderr(LogSeverity severity, const char *message, size_t len)
{
    ColoredWriteToStderrOrStdout(stderr, severity, message, len);
}

void
AddLogSink(LogSink::Ptr dest)
{
    std::lock_guard<std::mutex> _(g_sink_mutex);
    if (g_sinks.insert(dest).second) {
        g_sinks_arr.push_back(dest);
        g_sink_count.fetch_add(1);
    }
}

void
RemoveLogSink(LogSink::Ptr dest)
{
    std::lock_guard<std::mutex> _(g_sink_mutex);
    if (g_sinks.erase(dest) > 0) {
        auto iter = std::find(g_sinks_arr.begin(), g_sinks_arr.end(), dest);
        g_sinks_arr.erase(iter);
        g_sink_count.fetch_sub(1);
    }
}

void
SetStderrLogging(LogSeverity min_severity)
{}

void
LogToStderr()
{}

void
LogToSinks(LogSeverity severity,
           const char *full_filename,
           const char *base_filename,
           int line,
           const LogMessageTime &time,
           const char *message,
           size_t message_len)
{
    std::lock_guard<std::mutex> _(g_sink_mutex);
    for (auto &&sink : g_sinks_arr) {
        if (sink->ShouldLog(severity)) {
            try {
                sink->Send(severity, full_filename, base_filename, line, time, message, message_len);
            } catch (const std::exception &e) {
                fprintf(stderr, "\nLogSink[name=%s], reason=%s\n", sink->name(), e.what());
                assert(false && "Send log to LogSink Failed");
            }
        }
    }
}

void
WaitForSinks()
{
    std::lock_guard<std::mutex> _(g_sink_mutex);
    for (auto &&sink : g_sinks_arr) {
        try {
            sink->Flush();
        } catch (const std::exception &e) {
            fprintf(stderr, "\nLogSink[name=%s], reason=%s\n", sink->name(), e.what());
            assert(false && "Send log to LogSink Failed");
        }
    }
}

const char *
GetLogSeverityName(LogSeverity severity)
{
    const static std::map<LogSeverity, const char *> severity_names = {
        {TILE_INFO,    "INFO"   },
        {TILE_WARNING, "WARNING"},
        {TILE_ERROR,   "ERROR"  },
        {TILE_FATAL,   "FATAL"  },
    };

    auto iter = severity_names.find(severity);
    if (iter != severity_names.end()) {
        return iter->second;
    } else {
        return "UNKNOWN";
    }
}

}// namespace tile
