﻿// file:	kdbgout.c
#include "pch.h"
#include "sbcs.h"
#include "extend/sberror.h"
#include <signal.h>
#if _MSC_VER && _SB_WINDOWS_DESKTOP_
#include <dbghelp.h>
#endif
#if _SB_ANDROID_
#include <android/log.h>	// 링크에 liblog.so 필요
#endif

#define USE_OLV_PREFIX	1
#define USE_DBG_LOCK	1

#if USE_DBG_LOCK
#define DBG_LOCK			K_LOCK(k_impl_dbgout.lock)
#define DBG_UNLOCK			K_UNLOCK(k_impl_dbgout.lock)
#else
#define DBG_LOCK			k_p_lock()
#define DBG_UNLOCK			k_p_unlock()
#endif


//////////////////////////////////////////////////////////////////////////
// 출력 레벨

//
static char k_lvprefix(kint v)
{
	static char _k_olv_prefixs[] =
	{
		'A',
		'N',
		'R',
		'I',
		'W',
		'E',
		'C',
		'F',
		'U'
	};
	return _k_olv_prefixs[(ksize_t)v < K_COUNTOF(_k_olv_prefixs) ? (ksize_t)v : K_COUNTOF(_k_olv_prefixs) - 1];
}

//
static const char* k_lvname(kint v)
{
	static const char* _k_olv_names[] =
	{
		"all",
		"internal",
		"result",
		"info",
		"warning",
		"error",
		"critical",
		"fatal",
		"unknown"
	};
	return _k_olv_names[(ksize_t)v < K_COUNTOF(_k_olv_names) ? (ksize_t)v : K_COUNTOF(_k_olv_names) - 1];
}

// 안드로이드 레벨
#if _SB_ANDROID_
static kint k_lvandroid(kint v)
{
	static const kint s_andid[] =
	{
		ANDROID_LOG_VERBOSE,	// ALL
		ANDROID_LOG_DEBUG,		// INTERNAL
		ANDROID_LOG_VERBOSE,	// RESULT
		ANDROID_LOG_INFO,		// INFO
		ANDROID_LOG_WARN,		// WARNING
		ANDROID_LOG_ERROR,		// ERROR
		ANDROID_LOG_ERROR,		// CRITICAL
		ANDROID_LOG_FATAL,		// FATAL
		ANDROID_LOG_INFO,		// NONE
	};

	return (ksize_t)v < K_COUNTOF(s_andid) ? s_andid[v] : s_andid[0];
}
#endif


//////////////////////////////////////////////////////////////////////////
// 출력 및 디버그

//
static struct kImplDbgOut
{
	kint				level;
	kcham				isdbg;
	kcham				ischar;

	kchar				tag[32];

#if USE_DBG_LOCK
	kLock				lock;
#endif

#if _SB_WINDOWS_
	HANDLE				handle;
#else
	FILE*               fp;
#endif
} k_impl_dbgout =
{
	KLV_INFO,
	TRUE,
	TRUE,

	"sb",

#if USE_DBG_LOCK
	KLOCK_INIT,
#endif

	NULL,
};

// 
void _k_dbgout_init(void)
{
#if _SB_WINDOWS_
#	if _SB_WINDOWS_DESKTOP_
	k_impl_dbgout.handle = GetStdHandle(STD_OUTPUT_HANDLE);
#	else
	k_impl_dbgout.ischar = FALSE;
	k_impl_dbgout.handle = NULL;
#	endif
#else
#	if !_SB_MOBILE_
	k_impl_dbgout.fp = stdout;
#	else
	k_impl_dbgout.ischar = FALSE;
	k_impl_dbgout.fp = NULL;
#	endif
#endif
}

// 
void _k_dbgout_disp(void)
{
#if _SB_WINDOWS_
	if (k_impl_dbgout.handle != NULL && !k_impl_dbgout.ischar)
		CloseHandle(k_impl_dbgout.handle);
#else
	if (k_impl_dbgout.fp != NULL && !k_impl_dbgout.ischar)
		fclose(k_impl_dbgout.fp);
#endif
}

// 콘솔 출력
static void _k_dbg_console_str(const char* s, kint len)
{
	DBG_LOCK;
#if _SB_WINDOWS_
	if (k_impl_dbgout.handle != NULL)
	{
		DWORD wtn, dw = (DWORD)(len <= 0 ? k_strlen(s) : len);

#if _SB_WINDOWS_DESKTOP_
		if (!k_impl_dbgout.ischar)
			WriteFile(k_impl_dbgout.handle, s, dw, &wtn, NULL);
		else if (WriteConsoleA(k_impl_dbgout.handle, s, dw, &wtn, NULL) == 0)
			WriteFile(k_impl_dbgout.handle, s, dw, &wtn, NULL);
#else
		WriteFile(k_impl_dbgout.handle, s, dw, &wtn, NULL);
#endif
	}
#else
	if (k_impl_dbgout.fp != NULL)
		fputs(s, k_impl_dbgout.fp);
#endif
	DBG_UNLOCK;

	if (k_impl_dbgout.isdbg)
	{
#if _SB_WINDOWS_
		OutputDebugStringA(s);
#elif _SB_ANDROID_
		__android_log_print(ANDROID_LOG_VERBOSE, k_impl_dbgout.tag, "%s", s);
#endif
	}
}

// 문자 출력
static void _k_dbg_console_char(int ch)
{
	DBG_LOCK;
#if _SB_WINDOWS_
	if (k_impl_dbgout.handle != NULL)
	{
		DWORD wtn;

#if _SB_WINDOWS_DESKTOP_
		if (!k_impl_dbgout.ischar)
			WriteFile(k_impl_dbgout.handle, &ch, 1, &wtn, NULL);
		else if (WriteConsoleA(k_impl_dbgout.handle, &ch, 1, &wtn, NULL) == 0)
			WriteFile(k_impl_dbgout.handle, &ch, 1, &wtn, NULL);
#else
		WriteFile(k_impl_dbgout.handle, &ch, 1, &wtn, NULL);
#endif
	}
#else
	if (k_impl_dbgout.fp != NULL)
		fputc(ch, k_impl_dbgout.fp);
#endif
	DBG_UNLOCK;

	if (k_impl_dbgout.isdbg)
	{
#if _SB_WINDOWS_
		char sz[2];
		sz[0] = (char)ch;
		sz[1] = '\0';
		OutputDebugStringA(sz);
#elif _SB_ANDROID_
		__android_log_print(ANDROID_LOG_VERBOSE, k_impl_dbgout.tag, "%c", ch);
#endif
	}
}

// 문자열 출력
#if _SB_WINDOWS_ 
static void _k_dbg_console_windows(const kwchar* s, kint len)
{
	DBG_LOCK;
	if (k_impl_dbgout.handle != NULL)
	{
		DWORD wtn, dw = (DWORD)(len <= 0 ? k_wcslen(s) : len);

#if _SB_WINDOWS_DESKTOP_
		if (!k_impl_dbgout.ischar)
			WriteFile(k_impl_dbgout.handle, s, dw * sizeof(kwchar), &wtn, NULL);
		else if (WriteConsole(k_impl_dbgout.handle, s, dw, &wtn, NULL) == 0)
			WriteFile(k_impl_dbgout.handle, s, dw * sizeof(kwchar), &wtn, NULL);
#else
		WriteFile(k_impl_dbgout.handle, s, dw * sizeof(kwchar), &wtn, NULL);
#endif
	}
	DBG_UNLOCK;

	if (k_impl_dbgout.isdbg)
		OutputDebugString(s);
}
#endif

// 출력
static void _k_dbg_console_trace(int level, const char* cls, const char* text)
{
#if USE_OLV_PREFIX
	char cp = k_lvprefix(level);
#else
	const char* lv = k_lvname(level);
#endif
	const char* fmt;
	char* buf;
	int size;

	if (!cls)
		cls = k_impl_dbgout.tag;

#if USE_OLV_PREFIX
	fmt = "[%c/%s] %s\n";
	size = k_snprintf(NULL, 0, fmt, cp, cls, text);
	buf = k_local_new(size + 1, char);
	k_snprintf(buf, size + 1, fmt, cp, cls, text);
#else
	fmt = "[%s.%s] %s\n";
	size = k_snprintf(NULL, 0, fmt, cls, lv, text);
	buf = k_local_new(size + 1, char);
	k_snprintf(buf, size + 1, fmt, cls, lv, text);
#endif

	_k_dbg_console_str(buf, size);

	if (k_impl_dbgout.isdbg)
	{
#if _SB_WINDOWS_
		OutputDebugStringA(buf);
#elif _SB_ANDROID_
		buf[size-1] = '\0';
		__android_log_print(k_lvandroid(level), k_impl_dbgout.tag, "%s", buf);
#endif
	}

	//
	if (level == KLV_FATAL)
	{
#if _SB_WINDOWS_DESKTOP_
		MessageBoxA(NULL, text, NULL, MB_ICONERROR | MB_SETFOREGROUND);
#endif

		if (k_candebug())
			k_breakpoint();

		abort();
	}
}


//////////////////////////////////////////////////////////////////////////
// 꺼먼

/**
* 출력을 다른 곳으로 전환. 파일이나 표준 입출력 등.
* @param	filename	파일의 이름.
*/
void k_redirect(const char* filename)
{
	DBG_LOCK;

#if _SB_WINDOWS_
	if (!k_impl_dbgout.ischar)
		CloseHandle(k_impl_dbgout.handle);

	if (!filename || k_strieqv(filename, "stdout") || k_strieqv(filename, "stdin"))
	{
		k_impl_dbgout.ischar = TRUE;
#if _SB_WINDOWS_DESKTOP_
		k_impl_dbgout.handle = GetStdHandle(STD_OUTPUT_HANDLE);
#else
		k_impl_dbgout.handle = NULL;
#endif
	}
	else if (k_strieqv(filename, "stderr"))
	{
		k_impl_dbgout.ischar = TRUE;
#if _SB_WINDOWS_DESKTOP_
		k_impl_dbgout.handle = GetStdHandle(STD_ERROR_HANDLE);
#else
		k_impl_dbgout.handle = NULL;
#endif
	}
	else
	{
		HANDLE h;
		LARGE_INTEGER ll;
		kwchar wz[260];
#if !_SB_WINDOWS_DESKTOP_
		CREATEFILE2_EXTENDED_PARAMETERS cep;
#endif

		k_conv_utf8_to_utf16(wz, 260 - 1, filename, 0);

#if _SB_WINDOWS_DESKTOP_
		h = CreateFile(wz, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
#else
		k_zero_1(&cep);
		cep.dwFileAttributes = FILE_ATTRIBUTE_NORMAL;
		h = CreateFile2(wz, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, OPEN_ALWAYS, &cep);
#endif

		if (h == INVALID_HANDLE_VALUE)
		{
			// 실패하면 표준 출력으로
			k_impl_dbgout.ischar = TRUE;
#if _SB_WINDOWS_DESKTOP_
			k_impl_dbgout.handle = GetStdHandle(STD_OUTPUT_HANDLE);
#else
			k_impl_dbgout.handle = NULL;
#endif
		}
		else
		{
			k_impl_dbgout.ischar = FALSE;
			k_impl_dbgout.handle = h;

			ll.QuadPart = 0;
			SetFilePointerEx(h, ll, &ll, FILE_END);
		}
	}
#else
	if (k_impl_dbgout.fp != NULL && !k_impl_dbgout.ischar)
		fclose(k_impl_dbgout.fp);

	if (!filename || k_strieqv(filename, "stdout") || k_strieqv(filename, "stdin"))
	{
		k_impl_dbgout.ischar = TRUE;
#if !_SB_MOBILE_
		k_impl_dbgout.fp = stdout;
#else
		k_impl_dbgout.fp = NULL;
#endif
	}
	else if (k_strieqv(filename, "stderr"))
	{
		k_impl_dbgout.ischar = TRUE;
#if !_SB_MOBILE_
		k_impl_dbgout.fp = stderr;
#else
		k_impl_dbgout.fp = NULL;
#endif
	}
	else
	{
		FILE* fp = fopen(filename, "a+");

		if (!fp)
		{
			k_impl_dbgout.ischar = TRUE;
#if !_SB_MOBILE_
			k_impl_dbgout.fp = stdout;
#else
			k_impl_dbgout.fp = NULL;
#endif
		}
		else
		{
			k_impl_dbgout.ischar = FALSE;
			k_impl_dbgout.fp = fp;

			fseek(fp, 0, SEEK_END);
		}
	}
#endif

	DBG_UNLOCK;

	_k_dbg_console_char('\n');

	//
	if (!k_impl_dbgout.ischar)
	{
		kDateTime dt;
		char sz[260];

		k_now(&dt);

		k_snprintf(sz, 260 - 1, "output redirect on %04d-%02d-%02d %02d:%02d:%02d.%03d", dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.millisecond);

		_k_dbg_console_trace(KLV_INFO, NULL, sz);
	}
}

/**
* 출력 레벨을 얻는다.
* @return	출력 레벨.
*/
kint k_getlv(void)
{
	return k_impl_dbgout.level;
}

/**
* 출력 레벨을 쓴다.
* @param	value	넣을 출력 레벨.
*/
void k_setlv(kint value)
{
	k_return_if_fail((kuint)value <= KLV_NONE);
	k_impl_dbgout.level = value;
}

/**
* 태그를 설정한다.
* @param	str_len_32	태그 문자열, 최대 길이는 32글자.
*/
void k_settag(const char* str_len_32)
{
	DBG_LOCK;

	if (str_len_32 && *str_len_32)
		k_strncpy(k_impl_dbgout.tag, str_len_32, 32 - 1);
	else
	{
		k_impl_dbgout.tag[0] = 's';
		k_impl_dbgout.tag[1] = 'b';
		k_impl_dbgout.tag[2] = '\0';
	}

	DBG_UNLOCK;
}


//////////////////////////////////////////////////////////////////////////
// 아웃풋

/**
 * 문자 쓰기.
 * @param	ch	문자.
 */
void k_output(kint ch)
{
	_k_dbg_console_char(ch);
}

/**
 * 문자열 출력.
 * @param	s	문자열.
 */
void k_outputs(const char* s)
{
	_k_dbg_console_str(s, 0);
}

/**
 * 문자열 출력, 포맷.
 * @param	fmt	포맷 문자열.
 */
void k_outputf(const char* fmt, ...)
{
	int ret;
	char* buf;
	va_list va;

	va_start(va, fmt);
	ret = k_vsnprintf(NULL, 0, fmt, va);
	va_end(va);

	buf = k_local_new(ret + 1, char);
	va_start(va, fmt);
	k_vsnprintf(buf, ret + 1, fmt, va);
	va_end(va);

	_k_dbg_console_str(buf, ret);
}

/**
 * UTF-8 문자열 출력.
 * @param	s	문자열.
 */
void k_outputs_utf8(const char* s)
{
#if _SB_WINDOWS_
	kwchar* uni;
	kint ret;

	ret = (kint)k_conv_utf8_to_utf16(NULL, 0, s, 0);
	uni = k_local_new(ret + 1, kwchar);
	k_conv_utf8_to_utf16(uni, ret + 1, s, 0);

	_k_dbg_console_windows(uni, ret);
#else
	k_outputs(s);
#endif
}

/**
 * UTF-8 문자열 출력, 포맷.
 * @param	fmt	포맷 문자열.
 */
void k_outputf_utf8(const char* fmt, ...)
{
	int ret;
	char* buf;
	va_list va;
#if _SB_WINDOWS_
	kwchar* uni;
#endif

	va_start(va, fmt);
	ret = k_vsnprintf(NULL, 0, fmt, va);
	va_end(va);

	buf = k_local_new(ret + 1, char);
	va_start(va, fmt);
	k_vsnprintf(buf, ret + 1, fmt, va);
	va_end(va);

#if _SB_WINDOWS_
	ret = (kint)k_conv_utf8_to_utf16(NULL, 0, buf, 0);
	uni = k_local_new(ret + 1, kwchar);
	k_conv_utf8_to_utf16(uni, ret + 1, buf, 0);

	_k_dbg_console_windows(uni, ret);
#else
	_k_dbg_console_str(buf, ret);
#endif
}

/**
 * 트레이스 텍스트.
 * @param	level	출력 레벨.
 * @param	cls  	클래스 이름.
 * @param	text 	텍스트.
 */
void k_debug_outputs(kint level, const char* cls, const char* text)
{
	_k_dbg_console_trace(level, cls, text);
}

/**
 * 트레이스 텍스트, 포맷.
 * @param	level	출력 레벨.
 * @param	cls  	클래스 이름.
 * @param	fmt  	포맷 문자열.
 */
void k_debug_outputf(kint level, const char* cls, const char* fmt, ...)
{
	char* buf;
	int len;
	va_list va;

	va_start(va, fmt);
	len = k_vsnprintf(NULL, 0, fmt, va);
	va_end(va);

	buf = k_local_new(len + 1, char);
	va_start(va, fmt);
	k_vsnprintf(buf, len + 1, fmt, va);
	va_end(va);

	_k_dbg_console_trace(level, cls, buf);
}

/**
 * 오류처리용 assert.
 * @param	expr		표현.
 * @param	mesg		오류 내용.
 * @param	filename	파일의 파일 이름.
 * @param	line		파일의 줄 번호.
 * @return	kint.
 */
kint k_debug_assert(const char* expr, const char* mesg, const char* filename, kint line)
{
	char* buf;
	int len;

	len = k_snprintf(NULL, 0, "filename=\"%s\", line=%d", filename, line);
	buf = k_local_new(len + 1, char);
	k_snprintf(buf, len + 1, "filename=\"%s\", line=%d", filename, line);
	_k_dbg_console_trace(KLV_CRITICAL, "k.assert", buf);

	if (mesg)
		_k_dbg_console_trace(KLV_NONE, "k.assert", mesg);

	if (expr)
		_k_dbg_console_trace(KLV_NONE, "k.assert", expr);

	return 0;
}

/**
 * HALT 메시지.
 * @param	cls	클래스 이름.
 * @param	msg	출력할 메시지.
 */
kint k_debug_halt(const char* cls, const char* msg)
{
	const char* fmt;
	char* buf;
	int len;

	if (!cls)
		cls = k_impl_dbgout.tag;

	if (msg)
	{
		fmt = "HALT [%s] %s\n";
		len = k_snprintf(NULL, 0, fmt, cls, msg);
		buf = k_local_new(len + 1, char);
		k_snprintf(buf, len + 1, fmt, cls, msg);
	}
	else
	{
		fmt = "HALT [%s]\n";
		len = k_snprintf(NULL, 0, fmt, cls);
		buf = k_local_new(len + 1, char);
		k_snprintf(buf, len + 1, fmt, cls);
	}

	_k_dbg_console_str(buf, len);

	if (k_candebug())
		k_breakpoint();

	k_exit(255);

	return 0;
}


//////////////////////////////////////////////////////////////////////////
// 디버그 핸들링

#if _MSC_VER && _SB_WINDOWS_DESKTOP_
typedef BOOL(WINAPI* stub_MiniDumpFunc)(HANDLE, DWORD, HANDLE, MINIDUMP_TYPE, CONST PMINIDUMP_EXCEPTION_INFORMATION, CONST PMINIDUMP_USER_STREAM_INFORMATION, CONST PMINIDUMP_CALLBACK_INFORMATION);

// 미니 덤프 함수 준비
static stub_MiniDumpFunc _k_dbg_prep_mini_dump_func(void)
{
	stub_MiniDumpFunc func = NULL;
	HMODULE dll = LoadLibrary(L"dbghelp");

	if (dll)
	{
		func = (stub_MiniDumpFunc)GetProcAddress(dll, "MiniDumpWriteDump");

		if (!func)
			FreeLibrary(dll);
	}

	return func;
}

// 미니 덤프
static LONG _stdcall _k_dbg_mini_dump_filter(PEXCEPTION_POINTERS e)
{
	stub_MiniDumpFunc mdfn;
	kwchar tmp[MAX_PATH];
	kwchar mdn[MAX_PATH];
	kDateTime dt;
	HANDLE process;
	HANDLE hf;
	DWORD dw;
	MINIDUMP_EXCEPTION_INFORMATION di;

	// 함수가 있으면 고고씽
	if ((mdfn = _k_dbg_prep_mini_dump_func()) != NULL)
	{
		k_now(&dt);
		k_snwprintf(tmp, MAX_PATH - 1, L".%08X%08X", dt.date, dt.time);

		GetModuleFileName(NULL, mdn, MAX_PATH - 1);
		k_wcscat(mdn, tmp);
		k_wcscat(mdn, L".dmp");

		process = GetCurrentProcess();
		dw = GetCurrentProcessId();
		hf = CreateFile(mdn, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

		di.ExceptionPointers = e;
		di.ClientPointers = FALSE;
		di.ThreadId = GetCurrentThreadId();

		mdfn(process, dw, hf, MiniDumpNormal, e ? &di : NULL, NULL, NULL);
	}

	// 예의상 멘트 날려 줌
	k_snwprintf(tmp, 260 - 1, L"Application error found!.\nPlease contact service center.\n\n[@=0x%08X]", e->ExceptionRecord->ExceptionCode);
	MessageBox(NULL, tmp, L"Application exception", MB_OK | MB_SETFOREGROUND);

	return EXCEPTION_EXECUTE_HANDLER;
}
#endif

/** 오류 발생 처리기를 활성화한다 (윈도우의 MiniDump) */
void k_apphandler(void)
{
#if _MSC_VER && _SB_WINDOWS_DESKTOP_
	SetUnhandledExceptionFilter(_k_dbg_mini_dump_filter);
#endif
}

// 시스템 오류 메시지를 가져온다.
static kcham _k_syserr_value(kint err, char* buf, ksize_t size, kcham isutf8)
{
#if _SB_WINDOWS_
	DWORD dw;
	kwchar uni[256];

	k_return_value_if_fail(buf != NULL, FALSE);

	dw = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL,
		(DWORD)err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), uni, 256 - 1, NULL);

	if (dw == 0)
		k_strncpy(buf, "unknown error", size);
	else
	{
		uni[dw - 1] = L'\0';
		k_wcsrem(uni, L"\n\r");

		if (isutf8)
			k_conv_utf16_to_utf8(buf, size, uni, 0);
		else
			k_getasc(buf, size, uni);
	}

	return TRUE;
#else
	char* psz;

	k_return_value_if_fail(buf != NULL, FALSE);

	psz = strerror(err);

	if (!psz)
		k_strncpy(buf, "unknown error", size);
	else
		k_strncpy(buf, psz, size);

	return TRUE;
#endif
}

/**
 * 시스템 오류를 가져온다.
 * @param [반환]	buf			(널값이 아닌) 버퍼.
 * @param	size			버퍼의 크기.
 * @param	code_or_zero	오류 코드. 0이면 시스템 오류로 처리.
 * @return	성공하면 참, 실패하면 거짓.
 */
kcham k_syserr(char* buf, ksize_t size, kint code_or_zero)
{
#if _SB_WINDOWS_
	return _k_syserr_value(code_or_zero > 0 ? code_or_zero : GetLastError(), buf, size, FALSE);
#else
	return _k_syserr_value(code_or_zero > 0 ? code_or_zero : errno, buf, size, FALSE);
#endif
}

/**
 * 시스템 오류, UTF-8 버전.
 * @param [반환]	buf			(널값이 아닌) 버퍼.
 * @param	size			버퍼의 크기.
 * @param	code_or_zero	오류 코드. 0이면 시스템 오류로 처리.
 * @return	성공하면 참, 실패하면 거짓.
 */
kcham k_syserr_utf8(char* buf, ksize_t size, kint code_or_zero)
{
#if _SB_WINDOWS_
	return _k_syserr_value(code_or_zero > 0 ? code_or_zero : GetLastError(), buf, size, TRUE);
#else
	return _k_syserr_value(code_or_zero > 0 ? code_or_zero : errno, buf, size, TRUE);
#endif
}
