#pragma once

//#ifdef WIN32
//#ifndef _CRT_SECURE_NO_WARNINGS
//#define _CRT_SECURE_NO_WARNINGS
//#endif
//#include <Windows.h>
//#endif

/*
Warnning: 
The singleton pattern that log implemented is not thread-safe,
you should call log::get_instance() once in your main thread,
or else it might cause multiple constructions.
*/

// uncomment line below to enable OutputDebugString
// #define NO_WINDOWS

#include <assert.h>
#include <mutex>
#include <fstream>
#include <string>
#include <memory>
#include <cstring>
#include <cstdint>
#include <cstdarg>
#include <algorithm>
#include <boost/noncopyable.hpp>
#include "utf8.h"
#include "chrono_wrapper.h"
#include "singleton.h"

namespace jlib
{

#define JLOG jlib::log::log_utf16
#define JLOGA jlib::log::log_utf8
#define JLOGW jlib::log::log_utf16
#define JLOGB(b, l) jlib::log::dump_hex(b, l)
#define JLOGASC(b, l) jlib::log::dump_ascii(b, l)

//#define IMPLEMENT_CLASS_LOG_STATIC_MEMBER jlib::log* jlib::log::instance_ = nullptr;

class log : public dp::singleton<log>
{
	enum { max_output_size = 1024 * 64, max_single_log_file_size = 1024 * 1024 * 10 };

private:
	bool log_to_file_ = false;
	bool log_to_dbg_view_ = true;
	bool log_to_console_ = true;
	bool running_ = false;
	std::ofstream log_file_;
	std::string log_file_foler_ = "";
	std::string log_file_path_ = "";
	std::string line_prefix_ = "";
	std::mutex lock_;
	//static log* instance_;	

public:
	// initializers, they should be called right after get_instance
	void set_line_prifix(const std::string& prefix) { line_prefix_ = prefix; }

	void set_output_to_dbg_view(bool b = true) { log_to_dbg_view_ = b; }
	
	void set_output_to_console(bool b = true) { log_to_console_ = b; }

	void set_log_file_foler(const std::string& folder_path) { log_file_foler_ = folder_path.empty() ? "" : folder_path + "\\"; }

	void set_output_to_file(bool b = true) { log_to_file_ = b; if (b) create_file_name(); }

	auto get_log_file_path() const { return log_file_path_; }

public:
	
	//static log* get_instance()
	//{
	//	/*
	//	Warnning:
	//	The singleton pattern that log implemented is not thread-safe,
	//	you should call log::get_instance() once in your main thread,
	//	or else it might cause multiple constructions.
	//	*/
	//	if (log::instance_ == nullptr) {
	//		static log log_;
	//		log::instance_ = &log_;
	//	}
	//	return log::instance_;
	//}

	~log()
	{
		running_ = false;

		if (log_file_.is_open()) {
			log_file_.close();
		}

#ifdef _DEBUG
		output_to_dbg_view("log::~log\n");
#endif
	}

public:

	// static operations

	static void dump_hex(const char* buff, size_t buff_len)
	{
		try {
			auto instance = log::get_instance();

			if (instance->log_to_file_ || instance->log_to_dbg_view_ || instance->log_to_console_) {
				size_t output_len = buff_len * 6 + 64;
				std::unique_ptr<char[]> output = std::unique_ptr<char[]>(new char[output_len]);
				output[0] = 0;
				char c[64] = { 0 };
				std::sprintf(c, "len %d\n", buff_len);
				std::strcat(output.get(), c);
				for (size_t i = 0; i < buff_len; i++) {
					std::sprintf(c, "%02X ", static_cast<unsigned char>(buff[i]));
					std::strcat(output.get(), c);
					if (i > 0 && (i + 1) % 16 == 0) {
						std::strcat(output.get(), "\n");
					}
				}
				std::strcat(output.get(), "\n");
				instance->output(output.get());
			}
		} catch (...) {
			assert(0);
		}
	}

	static void dump_ascii(const char* buff, size_t buff_len)
	{
		try {
			auto instance = log::get_instance();

			if (instance->log_to_file_ || instance->log_to_dbg_view_ || instance->log_to_console_) {
				size_t output_len = buff_len * 6 + 64;
				std::unique_ptr<char[]> output = std::unique_ptr<char[]>(new char[output_len]);
				output[0] = 0;
				char c[64] = { 0 };
				std::sprintf(c, "len %d\n", buff_len);
				std::strcat(output.get(), c);
				for (size_t i = 0; i < buff_len; i++) {
					std::sprintf(c, "%C ", static_cast<unsigned char>(buff[i]));
					std::strcat(output.get(), c);
					if (i > 0 && (i + 1) % 16 == 0) {
						std::strcat(output.get(), "\n");
					}
				}
				std::strcat(output.get(), "\n");
				instance->output(output.get());
			}
		} catch (...) {
			assert(0);
		}
	}

	static void log_utf16(const wchar_t* format, ...)
	{
		try {
			auto instance = log::get_instance();

			if (instance->log_to_file_ || instance->log_to_dbg_view_ || instance->log_to_console_) {
				wchar_t buf[max_output_size], *p;
				p = buf;
				va_list args;
				va_start(args, format);
				p += _vsnwprintf_s(p, max_output_size - 1, max_output_size - 1, format, args);
				va_end(args);
				while ((p > buf) && (*(p - 1) == '\r' || *(p - 1) == '\n'))
					*--p = '\0';
				*p++ = '\r';
				*p++ = '\n';
				*p = '\0';

				//instance->output(instance->format_msg(utf8::w2a(buf)));
				std::lock_guard<std::mutex> lock(instance->lock_);
				auto msg = instance->format_msg(utf8::w2a(buf));
				if (instance->log_to_file_) {
					instance->output_to_log_file(msg);
				}

				if (instance->log_to_dbg_view_) {
#if defined(WIN32) && !defined(NO_WINDOWS)
					OutputDebugStringW(buf);
#else
					std::printf(msg.c_str());
#endif	
				}

				if (instance->log_to_console_) {
					std::wprintf(L"%s", buf);
				}
			}
		} catch (...) {
			assert(0);
		}
	}

	static void log_utf8(const char* format, ...)
	{
		try {
			auto instance = log::get_instance();

			if (instance->log_to_file_ || instance->log_to_dbg_view_ || instance->log_to_console_) {
				char buf[max_output_size], *p;
				p = buf;
				va_list args;
				va_start(args, format);
				p += _vsnprintf_s(p, max_output_size - 1, max_output_size - 1, format, args);
				va_end(args);
				while ((p > buf) && (*(p - 1) == '\r' || *(p - 1) == '\n'))
					*--p = '\0';
				*p++ = '\r';
				*p++ = '\n';
				*p = '\0';

				instance->output(instance->format_msg(buf));
			}
		} catch (...) {
			assert(0);
		}
	}


protected:

	log() {
		running_ = true;
		char out[128] = { 0 };
		sprintf(out, "log construction addr: %p\n", this);
		output_to_dbg_view(out);
	}

	bool open_log_file() {
		if (!log_file_.is_open()) {
			log_file_.open(log_file_path_);
		}
		return log_file_.is_open();
	}

	void create_file_name() {
		auto s = now_to_string();
		std::replace(s.begin(), s.end(), ' ', '_');
		std::replace(s.begin(), s.end(), ':', '-');
		log_file_path_ = log_file_foler_ + s + ".log";
	}

	std::string format_msg(const std::string& msg) {
		return line_prefix_ + " " + now_to_string(true) + " ---- " + msg;
	}

	void output(const std::string& msg) {
		std::lock_guard<std::mutex> lock(lock_);
		if (log_to_file_) {
			output_to_log_file(msg);
		}

		if (log_to_dbg_view_) {
			output_to_dbg_view(msg);
		}

		if (log_to_console_) {
			std::printf(msg.c_str());
		}
	}

	void output_to_log_file(const std::string& msg) {
		if (log_file_.is_open()) {
			auto size = log_file_.tellp();
			if (size > max_single_log_file_size) {
				log_file_.close();
				create_file_name();
				open_log_file();
			}
		}

		if (!log_file_.is_open()) {
			open_log_file();
		}

		if (log_file_.is_open()) {
			log_file_.write(msg.c_str(), msg.size());
			log_file_.flush();
		}
	}

	void output_to_dbg_view(const std::string& msg) {
#if defined(WIN32) && !defined(NO_WINDOWS)
		OutputDebugStringA(msg.c_str());
#else
		std::printf(msg.c_str());
#endif
	}

	

};


class log_function {
private:
	const char* func_name_;
	std::chrono::steady_clock::time_point begin_;

public:
	log_function(const char* func_name) : func_name_(func_name) {
		JLOGA("%s in\n", func_name_); begin_ = std::chrono::steady_clock::now();
	}

	~log_function() {
		auto diff = std::chrono::steady_clock::now() - begin_;
		auto msec = std::chrono::duration_cast<std::chrono::milliseconds>(diff);
		JLOGA("%s out, duration: %d(ms)\n", func_name_, msec.count());
	}
};

#define LOG_FUNCTION(func_name) jlib::log_function __log_function_object__(func_name);
#define AUTO_LOG_FUNCTION LOG_FUNCTION(__FUNCTION__);

class range_log
{
private:
	std::string msg_;
	std::chrono::steady_clock::time_point begin_;

public:
	range_log(const std::string& msg) : msg_(msg) {
		JLOGA((msg_ + " in").c_str());
		begin_ = std::chrono::steady_clock::now();
	}

	range_log(const std::wstring& msg) : msg_() { 
		msg_ = utf8::w2a(msg); 
		JLOGA((msg_ + " in").c_str());
		begin_ = std::chrono::steady_clock::now(); 
	}

	~range_log() {
		auto diff = std::chrono::steady_clock::now() - begin_;
		auto msec = std::chrono::duration_cast<std::chrono::milliseconds>(diff);
		JLOGA("%s out, duration: %d(ms)\n", msg_.c_str(), msec.count());
	}
};



};