#ifndef INSTRUMENTOR_HPP
#define INSTRUMENTOR_HPP

#include "open_engine/logging.hpp"

#include <mutex>
#include <sstream>
#include <string>
#include <chrono>
#include <algorithm>
#include <fstream>
#include <thread>

namespace OpenEngine {

    using FloatingPointMicroseconds = std::chrono::duration<double, std::micro>;

    struct ProfileResult
    {
        std::string name;
        FloatingPointMicroseconds start;
		std::chrono::microseconds elapsed_time;
        std::thread::id thread_id;
    };

    struct InstrumentationSession
    {
        const char* name;
    };

    class Instrumentor
    {
        public:
            ~Instrumentor()
            {
                EndSession();
            };

            void BeginSession(const char* name, const std::string& filepath = "results.json")
            {
                std::lock_guard lock(mutex);
                if (current_session) {
                    if (Logger::GetCoreLogger())
                        OE_CORE_ERROR("Instrumentor::BeginSession({}), when session {} already exists", name, current_session->name);
                    InternalEndSession();
                }

                output_stream.open(filepath);
                if(output_stream.is_open()) {
                    current_session = new InstrumentationSession(name);
                    WriteHeader();
                } else {
                    if (Logger::GetCoreLogger())
                        OE_CORE_ERROR("Instrumentor could not open results file: {}", filepath);
                }
            };

            void EndSession()
            {
                std::lock_guard lock(mutex);
                InternalEndSession();
            };

            void WriteProfile(const ProfileResult& result)
            {
                std::stringstream json;

                std::string name = result.name;
                std::replace(name.begin(), name.end(), '"', '\'');

                json << std::setprecision(3) << std::fixed;
                json << ",{";
                json << "\"cat\":\"function\",";
                json << "\"dur\":" << (result.elapsed_time.count()) << ',';
                json << "\"name\":\"" << name << "\",";
                json << "\"ph\":\"X\",";
                json << "\"pid\":0,";
                json << "\"tid\":" << result.thread_id << ",";
                json << "\"ts\":" << result.start.count();
                json << "}";

                std::lock_guard lock(mutex);

                if (current_session) {
                    output_stream << json.str();
                    output_stream.flush();
                }
            };

            static Instrumentor& Get()
            {
                static Instrumentor instance;
                return instance;
            };

        private:
            void WriteHeader()
            {
                output_stream << "{\"otherData\": {},\"traceEvents\":[{}";
            };

            void WriteFooter()
            {
                output_stream << "]}";
            };

            void InternalEndSession() {
                if (current_session) {
                    WriteFooter();
                    output_stream.close();
                    delete current_session;
                    current_session = nullptr;
                }
            }

            Instrumentor()
            {
            };

        private:
            std::mutex mutex;
            InstrumentationSession* current_session = nullptr;
            std::ofstream output_stream;
            int profile_count = 0;
    };

    class InstrumentationTimer
    {
        public:
            InstrumentationTimer(const char* name)
                : name(name), stopped(false)
            {
                start_timepoint = std::chrono::high_resolution_clock::now();
            };

            ~InstrumentationTimer()
            {
                if (!stopped)
                    Stop();
            };

            void Stop()
            {
                auto end_timepoint = std::chrono::high_resolution_clock::now();

                auto start = FloatingPointMicroseconds{start_timepoint.time_since_epoch()};
                auto elapsed_time = std::chrono::time_point_cast<std::chrono::microseconds>(end_timepoint).time_since_epoch()
                    - std::chrono::time_point_cast<std::chrono::microseconds>(start_timepoint).time_since_epoch();

                uint32_t thread_id = std::hash<std::thread::id>{}(std::this_thread::get_id());
                Instrumentor::Get().WriteProfile({ name, start, elapsed_time, std::this_thread::get_id() });

                stopped = true;
            };

        private:
            const char* name;
            std::chrono::time_point<std::chrono::high_resolution_clock> start_timepoint;
            bool stopped;
    };
}

#define OE_PROFILE 1
#if OE_PROFILE
	#define OE_PROFILE_BEGIN_SESSION(name, filepath) ::OpenEngine::Instrumentor::Get().BeginSession(name, filepath)
	#define OE_PROFILE_END_SESSION() ::OpenEngine::Instrumentor::Get().EndSession()
	#define OE_PROFILE_SCOPE(name) ::OpenEngine::InstrumentationTimer timer##__LINE__(name);
	#define OE_PROFILE_FUNCTION() OE_PROFILE_SCOPE(__PRETTY_FUNCTION__)
#else
	#define OE_PROFILE_BEGIN_SESSION(name, filepath)
	#define OE_PROFILE_END_SESSION()
	#define OE_PROFILE_SCOPE(name)
	#define OE_PROFILE_FUNCTION()
#endif

#endif // INSTRUMENTOR_HPP
