#include "clar_libgit2_trace.h"
#include "clar_libgit2.h"
#include "clar_libgit2_timer.h"
#include "trace.h"

struct method {
	const char *name;
	void (*git_trace_cb)(git_trace_level_t level, const char *msg);
	void (*close)(void);
};

static const char *message_prefix(git_trace_level_t level)
{
	switch (level) {
	case GIT_TRACE_NONE:
		return "[NONE]:  ";
	case GIT_TRACE_FATAL:
		return "[FATAL]: ";
	case GIT_TRACE_ERROR:
		return "[ERROR]: ";
	case GIT_TRACE_WARN:
		return "[WARN]:  ";
	case GIT_TRACE_INFO:
		return "[INFO]:  ";
	case GIT_TRACE_DEBUG:
		return "[DEBUG]: ";
	case GIT_TRACE_TRACE:
		return "[TRACE]: ";
	default:
		return "[?????]: ";
	}
}

static void _git_trace_cb__printf(git_trace_level_t level, const char *msg)
{
	printf("%s%s\n", message_prefix(level), msg);
}

#if defined(GIT_WIN32)
static void _git_trace_cb__debug(git_trace_level_t level, const char *msg)
{
	OutputDebugString(message_prefix(level));
	OutputDebugString(msg);
	OutputDebugString("\n");

	printf("%s%s\n", message_prefix(level), msg);
}
#else
#define _git_trace_cb__debug _git_trace_cb__printf
#endif


static void _trace_printf_close(void)
{
	fflush(stdout);
}

#define _trace_debug_close _trace_printf_close


static struct method s_methods[] = {
	{ "printf", _git_trace_cb__printf, _trace_printf_close },
	{ "debug",  _git_trace_cb__debug,  _trace_debug_close  },
	/* TODO add file method */
	{0},
};


static int s_trace_loaded = 0;
static int s_trace_level = GIT_TRACE_NONE;
static struct method *s_trace_method = NULL;
static int s_trace_tests = 0;

static int set_method(const char *name)
{
	int k;

	if (!name || !*name)
		name = "printf";

	for (k=0; (s_methods[k].name); k++) {
		if (strcmp(name, s_methods[k].name) == 0) {
			s_trace_method = &s_methods[k];
			return 0;
		}
	}
	fprintf(stderr, "Unknown CLAR_TRACE_METHOD: '%s'\n", name);
	return -1;
}


/**
 * Lookup CLAR_TRACE_LEVEL and CLAR_TRACE_METHOD from
 * the environment and set the above s_trace_* fields.
 *
 * If CLAR_TRACE_LEVEL is not set, we disable tracing.
 *
 * TODO If set, we assume GIT_TRACE_TRACE level, which
 * logs everything. Later, we may want to parse the
 * value of the environment variable and set a specific
 * level.
 *
 * We assume the "printf" method.  This can be changed
 * with the CLAR_TRACE_METHOD environment variable.
 * Currently, this is only needed on Windows for a "debug"
 * version which also writes to the debug output window
 * in Visual Studio.
 *
 * TODO add a "file" method that would open and write
 * to a well-known file. This would help keep trace
 * output and clar output separate.
 *
 */
static void _load_trace_params(void)
{
	char *sz_level;
	char *sz_method;
	char *sz_tests;

	s_trace_loaded = 1;

	sz_level = cl_getenv("CLAR_TRACE_LEVEL");
	if (!sz_level || !*sz_level) {
		s_trace_level = GIT_TRACE_NONE;
		s_trace_method = NULL;
		return;
	}

	/* TODO Parse sz_level and set s_trace_level. */
	s_trace_level = GIT_TRACE_TRACE;

	sz_method = cl_getenv("CLAR_TRACE_METHOD");
	if (set_method(sz_method) < 0)
		set_method(NULL);

	sz_tests = cl_getenv("CLAR_TRACE_TESTS");
	if (sz_tests != NULL)
		s_trace_tests = 1;
}

#define HR "================================================================"

/**
 * Timer to report the take spend in a test's run() method.
 */
static cl_perf_timer s_timer_run = CL_PERF_TIMER_INIT;

/**
 * Timer to report total time in a test (init, run, cleanup).
 */
static cl_perf_timer s_timer_test = CL_PERF_TIMER_INIT;

static void _cl_trace_cb__event_handler(
	cl_trace_event ev,
	const char *suite_name,
	const char *test_name,
	void *payload)
{
	GIT_UNUSED(payload);

	if (!s_trace_tests)
		return;

	switch (ev) {
	case CL_TRACE__SUITE_BEGIN:
		git_trace(GIT_TRACE_TRACE, "\n\n%s\n%s: Begin Suite", HR, suite_name);
#if 0 && defined(GIT_WIN32_LEAKCHECK)
		git_win32__crtdbg_stacktrace__dump(
			GIT_WIN32__CRTDBG_STACKTRACE__SET_MARK,
			suite_name);
#endif
		break;

	case CL_TRACE__SUITE_END:
#if 0 && defined(GIT_WIN32_LEAKCHECK)
		/* As an example of checkpointing, dump leaks within this suite.
		 * This may generate false positives for things like the global
		 * TLS error state and maybe the odb cache since they aren't
		 * freed until the global shutdown and outside the scope of this
		 * set of tests.
		 *
		 * This may under-report if the test itself uses a checkpoint.
		 * See tests/trace/windows/stacktrace.c
		 */
		git_win32__crtdbg_stacktrace__dump(
			GIT_WIN32__CRTDBG_STACKTRACE__LEAKS_SINCE_MARK,
			suite_name);
#endif
		git_trace(GIT_TRACE_TRACE, "\n\n%s: End Suite\n%s", suite_name, HR);
		break;

	case CL_TRACE__TEST__BEGIN:
		git_trace(GIT_TRACE_TRACE, "\n%s::%s: Begin Test", suite_name, test_name);
		cl_perf_timer__init(&s_timer_test);
		cl_perf_timer__start(&s_timer_test);
		break;

	case CL_TRACE__TEST__END:
		cl_perf_timer__stop(&s_timer_test);
		git_trace(GIT_TRACE_TRACE, "%s::%s: End Test (%" PRIuZ " %" PRIuZ ")", suite_name, test_name,
				  cl_perf_timer__last(&s_timer_run),
				  cl_perf_timer__last(&s_timer_test));
		break;

	case CL_TRACE__TEST__RUN_BEGIN:
		git_trace(GIT_TRACE_TRACE, "%s::%s: Begin Run", suite_name, test_name);
		cl_perf_timer__init(&s_timer_run);
		cl_perf_timer__start(&s_timer_run);
		break;

	case CL_TRACE__TEST__RUN_END:
		cl_perf_timer__stop(&s_timer_run);
		git_trace(GIT_TRACE_TRACE, "%s::%s: End Run", suite_name, test_name);
		break;

	case CL_TRACE__TEST__LONGJMP:
		cl_perf_timer__stop(&s_timer_run);
		git_trace(GIT_TRACE_TRACE, "%s::%s: Aborted", suite_name, test_name);
		break;

	default:
		break;
	}
}

/**
 * Setup/Enable git_trace() based upon settings user's environment.
 */
void cl_global_trace_register(void)
{
	if (!s_trace_loaded)
		_load_trace_params();

	if (s_trace_level == GIT_TRACE_NONE)
		return;
	if (s_trace_method == NULL)
		return;
	if (s_trace_method->git_trace_cb == NULL)
		return;

	git_trace_set(s_trace_level, s_trace_method->git_trace_cb);
	cl_trace_register(_cl_trace_cb__event_handler, NULL);
}

/**
 * If we turned on git_trace() earlier, turn it off.
 *
 * This is intended to let us close/flush any buffered
 * IO if necessary.
 *
 */
void cl_global_trace_disable(void)
{
	cl_trace_register(NULL, NULL);
	git_trace_set(GIT_TRACE_NONE, NULL);
	if (s_trace_method && s_trace_method->close)
		s_trace_method->close();

	/* Leave s_trace_ vars set so they can restart tracing
	 * since we only want to hit the environment variables
	 * once.
	 */
}
