#include <psemek/log/log.hpp>
#include <psemek/util/to_string.hpp>
#include <psemek/util/exception.hpp>
#include <psemek/util/hash_table.hpp>

#include <chrono>
#include <ctime>
#include <iostream>
#include <sstream>
#include <iomanip>
#include <mutex>
#include <thread>
#include <atomic>
#include <vector>
#include <csignal>

namespace psemek::log
{

	namespace
	{

		std::atomic<std::size_t> max_thread_name_length = 3;

		std::mutex thread_names_mutex;
		util::hash_map<std::thread::id, std::unique_ptr<std::string>> thread_names;

		std::mutex sinks_mutex;
		std::vector<std::unique_ptr<sink>> sinks;

		struct signal_data
		{
			int signal;
			std::string message;
		};

		signal_data const signals[] =
		{
			{SIGINT,  "Aborting due to SIGINT" },
			{SIGILL,  "Aborting due to SIGILL" },
			{SIGABRT, "Aborting due to SIGABRT"},
			{SIGFPE,  "Aborting due to SIGFPE" },
			{SIGSEGV, "Aborting due to SIGSEGV"},
			{SIGTERM, "Aborting due to SIGTERM"},
		};

		struct signal_handler
		{
			signal_handler()
			{
				for (auto const & p : signals)
					std::signal(p.signal, &handler);
			}

		private:
			static void handler(int signal)
			{
				for (auto const & p : signals)
				{
					if (p.signal == signal)
					{
						log::error() << p.message;
						log::error() << util::stacktrace();
						break;
					}
				}

				{
					std::lock_guard lock{sinks_mutex};
					for (auto & sink : sinks)
						sink->flush();
					sinks.clear();
				}

				std::signal(signal, SIG_DFL);
				std::raise(signal);
			}
		} signal_handler;

		void put_message(level l, std::string const & str, std::string const & thread_name)
		{
			message msg
			{
				clock::now(),
				thread_name,
				l,
				str
			};

			std::lock_guard lock{sinks_mutex};
			for (auto const & sink : sinks)
				sink->put_message(msg);
		}

		static ::tm safe_localtime(std::time_t const & time)
		{
			static std::mutex mutex;
			std::lock_guard lock{mutex};
			return *std::localtime(&time);
		}

		struct default_sink_impl
			: sink
		{
			default_sink_impl(std::unique_ptr<io::ostream> stream, level min, level max)
				: stream_(std::move(stream))
				, min_(min)
				, max_(max)
			{}

			void put_message(message const & msg) override
			{
				if (msg.level < min_ || msg.level > max_) return;

				auto const time = clock::to_time_t(msg.time);
				auto const tm = safe_localtime(time);

				auto const millis = std::chrono::duration_cast<std::chrono::milliseconds>(msg.time.time_since_epoch()).count() % 1000;

				std::ostringstream os;
				os
					<< '[' << std::put_time(&tm, "%Y %b %d  %H:%M:%S.") << std::setw(3) << std::setfill('0') << millis << ']'
					<< '[' << std::setw(max_thread_name_length) << std::setfill(' ') << msg.thread_name << ']'
					<< '[' << std::setw(5) << msg.level << ']'
					<< ' ' << msg.message << '\n';

				auto const str = os.str();
				stream_->write(str.data(), str.size());
			}

			void flush() override
			{
				stream_->flush();
			}

		private:
			std::unique_ptr<io::ostream> stream_;
			level min_;
			level max_;
		};

	}

	void register_thread(std::string name)
	{
#ifdef _PTHREAD_H
#if (defined _GNU_SOURCE)
#if __APPLE__
		::pthread_setname_np(name.data());
#else
#if !(defined __EMSCRIPTEN__) || (defined __EMSCRIPTEN_PTHREADS__)
		::pthread_setname_np(::pthread_self(), name.data());
#endif
#endif
#endif
#endif

		auto id = std::this_thread::get_id();

		{
			std::lock_guard lock{thread_names_mutex};

			auto it = thread_names.find(id);
			if (it != thread_names.end())
				throw util::exception("Thread \"" + name + "\" already registered!");

			thread_names[id] = std::make_unique<std::string>(name);
		}

		{
			std::size_t current_max = max_thread_name_length.load();
			while(current_max < name.size() && !max_thread_name_length.compare_exchange_weak(current_max, name.size()))
			{}
		}

		put_message(level::info, "Thread \"" + name + "\" registered");
	}

	void unregister_thread(std::thread::id id)
	{
		std::string name;
		{
			std::lock_guard lock{thread_names_mutex};

			auto it = thread_names.find(id);
			if (it == thread_names.end())
			{
				std::ostringstream os;
				os << "Thread " << id << " not found!";
				throw util::exception(os.str());
			}

			name = std::move(*(it->second));
			thread_names.erase(it);
		}

		if (id == std::this_thread::get_id())
			put_message(level::info, "Thread \"" + name + "\" unregistered", name);
		else
			put_message(level::info, "Thread \"" + name + "\" unregistered");
	}

	std::unique_ptr<sink> default_sink(std::unique_ptr<io::ostream> stream, level min, level max)
	{
		return std::make_unique<default_sink_impl>(std::move(stream), min, max);
	}

	sink * add_sink(std::unique_ptr<sink> s)
	{
		auto ptr = s.get();
		std::lock_guard lock{sinks_mutex};
		sinks.push_back(std::move(s));
		return ptr;
	}

	std::unique_ptr<sink> remove_sink(sink * stream)
	{
		std::lock_guard lock{sinks_mutex};
		for (std::size_t i = 0; i < sinks.size(); ++i)
		{
			if (sinks[i].get() == stream)
			{
				auto result = std::move(sinks[i]);
				sinks.erase(sinks.begin() + i);
				return result;
			}
		}
		return nullptr;
	}

	void put_message(level l, std::string const & message)
	{
		static std::string unknown_thread_name = "???";

		auto const id = std::this_thread::get_id();

		std::string const * thread_name = nullptr;
		{
			std::lock_guard lock{thread_names_mutex};
			auto const it = thread_names.find(id);
			if (it == thread_names.end())
				thread_name = &unknown_thread_name;
			else
				thread_name = it->second.get();
		}

		put_message(l, message, *thread_name);
	}

}
