//////////////////////////////////////////////////////////////////////////////
//
// Copyright (c) 2004-2023 musikcube team
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//    * Redistributions of source code must retain the above copyright notice,
//      this list of conditions and the following disclaimer.
//
//    * Redistributions in binary form must reproduce the above copyright
//      notice, this list of conditions and the following disclaimer in the
//      documentation and/or other materials provided with the distribution.
//
//    * Neither the name of the author nor the names of other contributors may
//      be used to endorse or promote products derived from this software
//      without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
//////////////////////////////////////////////////////////////////////////////

#include <pch.hpp>
#include <musikcore/debug.h>
#include <musikcore/support/Common.h>
#include <functional>
#include <string>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <memory>
#include <iostream>
#include <time.h>

using namespace musik::core;

class log_queue;

static std::vector<std::unique_ptr<musik::debug::IBackend>> backends;
static std::thread* thread = nullptr;
static log_queue* queue = nullptr;
static std::recursive_mutex mutex;
static volatile bool cancel = true;

enum class debug_level {
    verbose = 0,
    info = 1,
    warning = 2,
    error = 3
};

class log_queue {
    public:
        struct log_entry {
            log_entry(debug_level l, const std::string& t, const std::string& m) {
                level = l;
                tag = t;
                message = m;
            }

            debug_level level;
            std::string tag;
            std::string message;
        };

        class stopped_exception {
        };

        log_queue() {
            active = true;
        }

        log_entry* pop_top() {
            std::unique_lock<std::mutex> lock(queue_mutex);
            while ((queue.size() == 0) && (active == true)) {
                wait_for_next_item_condition.wait(lock);
            }

            if (!active) {
                return nullptr;
            }

            log_entry* top = queue.front();
            queue.pop();
            return top;
        }

        bool push(log_entry* f) {
            std::unique_lock<std::mutex> lock(queue_mutex);

            if (active) {
                bool was_empty = (queue.size() == 0);
                queue.push(f);

                if (was_empty) {
                    wait_for_next_item_condition.notify_one();
                }

                return true;
            }

            return false;
        }

        void stop() {
            std::unique_lock<std::mutex> lock(queue_mutex);
            active = false;

            while (queue.size() > 0) {
                log_entry* top = queue.front();
                queue.pop();
                delete top;
            }

            wait_for_next_item_condition.notify_all();
        }

    private:
        std::queue<log_entry*> queue;
        std::condition_variable wait_for_next_item_condition;
        std::mutex queue_mutex;
        volatile bool active;
};

static void thread_proc() {
    try {
        while (!cancel) {
            log_queue::log_entry* entry = queue->pop_top();
            if (entry) {
                for (auto& backend : backends) {
                    switch (entry->level) {
                        case debug_level::verbose:
                            backend->verbose(entry->tag, entry->message);
                            break;
                        case debug_level::info:
                            backend->info(entry->tag, entry->message);
                            break;
                        case debug_level::warning:
                            backend->warning(entry->tag, entry->message);
                            break;
                        case debug_level::error:
                            backend->error(entry->tag, entry->message);
                            break;
                    }
                }
                delete entry;
            }
        }
    }
    catch (log_queue::stopped_exception&) {
    }
}

void musik::debug::Start(std::vector<musik::debug::IBackend*> backends) {
    std::unique_lock<std::recursive_mutex> lock(mutex);

    if (queue || thread) {
        return;
    }

    for (auto backend : backends) {
        ::backends.push_back(std::unique_ptr<musik::debug::IBackend>(backend));
    }

    cancel = false;
    queue = new log_queue();
    thread = new std::thread(std::bind(&thread_proc));

    info("LOG SESSION", "---------- START ----------");
}

void musik::debug::Shutdown() {
    std::unique_lock<std::recursive_mutex> lock(mutex);

    cancel = true;

    if (thread && queue) {
        queue->stop();
        thread->join();

        delete thread;
        thread = nullptr;
        delete queue;
        queue = nullptr;
    }

    backends.clear();
}

static void enqueue(debug_level level, const std::string& tag, const std::string& string) noexcept {
    try {
        std::unique_lock<std::recursive_mutex> lock(mutex);

        if (queue) {
            queue->push(new log_queue::log_entry(level, tag, string));
        }
    }
    catch (...) {
        fprintf(stderr, "[%d] [%s] %s", static_cast<int>(level), tag.c_str(), string.c_str());
    }
}

void musik::debug::verbose(const std::string& tag, const std::string& string) noexcept {
    enqueue(debug_level::verbose, tag, string);
}

void musik::debug::v(const std::string& tag, const std::string& string) noexcept {
    enqueue(debug_level::verbose, tag, string);
}

void musik::debug::info(const std::string& tag, const std::string& string) noexcept {
    enqueue(debug_level::info, tag, string);
}

void musik::debug::i(const std::string& tag, const std::string& string) noexcept {
    enqueue(debug_level::info, tag, string);
}

void musik::debug::warning(const std::string& tag, const std::string& string) noexcept {
    enqueue(debug_level::warning, tag, string);
}

void musik::debug::w(const std::string& tag, const std::string& string) noexcept {
    enqueue(debug_level::warning, tag, string);
}

void musik::debug::error(const std::string& tag, const std::string& string) noexcept {
    enqueue(debug_level::error, tag, string);
}

void musik::debug::e(const std::string& tag, const std::string& string) noexcept {
    enqueue(debug_level::error, tag, string);
}

////////// backend utils //////////

namespace musik {

    static std::string timestamp() {
        time_t rawtime = { 0 };
        struct tm * timeinfo;
        char buffer [64];
        time(&rawtime);
        timeinfo = localtime (&rawtime);
        strftime (buffer, sizeof(buffer), "%T", timeinfo);
        return std::string(buffer);
    }

    static void writeTo(
        std::ostream& out,
        const std::string& level,
        const std::string& tag,
        const std::string& message)
    {
        out << timestamp() << " [" << level << "] [" << tag << "] " << message << "\n";
        out.flush();
    }

}

////////// FileBackend //////////

namespace musik {

    debug::FileBackend::FileBackend(const std::string& fn)
    : out(fn.c_str()) {

    }

    debug::FileBackend::FileBackend(FileBackend&& fn) {
        this->out.swap(fn.out);
    }

    debug::FileBackend::~FileBackend() {
    }

    void debug::FileBackend::verbose(const std::string& tag, const std::string& string) {
        writeTo(this->out, "verbose", tag, string);
    }

    void debug::FileBackend::info(const std::string& tag, const std::string& string) {
        writeTo(this->out, "info", tag, string);
    }

    void debug::FileBackend::warning(const std::string& tag, const std::string& string) {
        writeTo(this->out, "warning", tag, string);
    }

    void debug::FileBackend::error(const std::string& tag, const std::string& string) {
        writeTo(this->out, "error", tag, string);
    }

}

////////// SimpleFileBackend //////////

namespace musik {

    debug::SimpleFileBackend::SimpleFileBackend()
    : FileBackend(GetDataDirectory() + "log.txt") {
    }

}

////////// ConsoleBackend //////////

namespace musik {

    debug::ConsoleBackend::ConsoleBackend() {
    }

    debug::ConsoleBackend::~ConsoleBackend() {
    }

    void debug::ConsoleBackend::verbose(const std::string& tag, const std::string& string) {
        writeTo(std::cout, "verbose", tag, string);
    }

    void debug::ConsoleBackend::info(const std::string& tag, const std::string& string) {
        writeTo(std::cout, "info", tag, string);
    }

    void debug::ConsoleBackend::warning(const std::string& tag, const std::string& string) {
        writeTo(std::cout, "warning", tag, string);
    }

    void debug::ConsoleBackend::error(const std::string& tag, const std::string& string) {
        writeTo(std::cerr, "error", tag, string);
    }

}
