//+private
package testing

import "base:runtime"
import "core:encoding/ansi"
import "core:fmt"
import "core:io"
import "core:mem"
import "core:path/filepath"
import "core:strings"

// Definitions of colors for use in the test runner.
SGR_RESET   :: ansi.CSI + ansi.RESET           + ansi.SGR
SGR_READY   :: ansi.CSI + ansi.FG_BRIGHT_BLACK + ansi.SGR
SGR_RUNNING :: ansi.CSI + ansi.FG_YELLOW       + ansi.SGR
SGR_SUCCESS :: ansi.CSI + ansi.FG_GREEN        + ansi.SGR
SGR_FAILED  :: ansi.CSI + ansi.FG_RED          + ansi.SGR

MAX_PROGRESS_WIDTH :: 100

// More than enough bytes to cover long package names, long test names, dozens
// of ANSI codes, et cetera.
LINE_BUFFER_SIZE :: (MAX_PROGRESS_WIDTH * 8 + 224) * runtime.Byte

PROGRESS_COLUMN_SPACING :: 2

Package_Run :: struct {
	name: string,
	header: string,

	frame_ready: bool,

	redraw_buffer: [LINE_BUFFER_SIZE]byte,
	redraw_string: string,

	last_change_state: Test_State,
	last_change_name: string,

	tests: []Internal_Test,
	test_states: []Test_State,
}

Report :: struct {
	packages: []Package_Run,
	packages_by_name: map[string]^Package_Run,

	pkg_column_len: int,
	test_column_len: int,
	progress_width: int,

	all_tests: []Internal_Test,
	all_test_states: []Test_State,
}

// Organize all tests by package and sort out test state data.
make_report :: proc(internal_tests: []Internal_Test) -> (report: Report, error: runtime.Allocator_Error) {
	assert(len(internal_tests) > 0, "make_report called with no tests")

	packages: [dynamic]Package_Run

	report.all_tests = internal_tests
	report.all_test_states = make([]Test_State, len(internal_tests)) or_return

	// First, figure out what belongs where.
	#no_bounds_check cur_pkg := internal_tests[0].pkg
	pkg_start: int

	// This loop assumes the tests are sorted by package already.
	for it, index in internal_tests {
		if cur_pkg != it.pkg {
			#no_bounds_check { 
				append(&packages, Package_Run {
					name = cur_pkg,
					tests = report.all_tests[pkg_start:index],
					test_states = report.all_test_states[pkg_start:index],
				}) or_return
			}

			when PROGRESS_WIDTH == 0 {
				report.progress_width = max(report.progress_width, index - pkg_start)
			}

			pkg_start = index
			report.pkg_column_len = max(report.pkg_column_len, len(cur_pkg))
			cur_pkg = it.pkg
		}
		report.test_column_len = max(report.test_column_len, len(it.name))
	}

	// Handle the last (or only) package.
	#no_bounds_check {
		append(&packages, Package_Run {
			name = cur_pkg,
			header = cur_pkg,
			tests = report.all_tests[pkg_start:],
			test_states = report.all_test_states[pkg_start:],
		}) or_return
	}
	when PROGRESS_WIDTH == 0 {
		report.progress_width = max(report.progress_width, len(internal_tests) - pkg_start)
	} else {
		report.progress_width = PROGRESS_WIDTH
	}
	report.progress_width = min(report.progress_width, MAX_PROGRESS_WIDTH)

	report.pkg_column_len = PROGRESS_COLUMN_SPACING + max(report.pkg_column_len, len(cur_pkg))

	shrink(&packages) or_return

	for &pkg in packages {
		pkg.header = fmt.aprintf("%- *[1]s[", pkg.name, report.pkg_column_len)
		assert(len(pkg.header) > 0, "Error allocating package header string.")

		// This is safe because the array is done resizing, and it has the same
		// lifetime as the map.
		report.packages_by_name[pkg.name] = &pkg
	}

	// It's okay to discard the dynamic array's allocator information here,
	// because its capacity has been shrunk to its length, it was allocated by
	// the caller's context allocator, and it will be deallocated by the same.
	//
	// `delete_slice` is equivalent to `delete_dynamic_array` in this case.
	report.packages = packages[:]

	return
}

destroy_report :: proc(report: ^Report) {
	for pkg in report.packages {
		delete(pkg.header)
	}

	delete(report.packages)
	delete(report.packages_by_name)
	delete(report.all_test_states)
}

redraw_package :: proc(w: io.Writer, report: Report, pkg: ^Package_Run) {
	if pkg.frame_ready {
		io.write_string(w, pkg.redraw_string)
		return
	}

	// Write the output line here so we can cache it.
	line_builder := strings.builder_from_bytes(pkg.redraw_buffer[:])
	line_writer  := strings.to_writer(&line_builder)

	highest_run_index: int
	failed_count: int
	done_count: int
	#no_bounds_check for i := 0; i < len(pkg.test_states); i += 1 {
		switch pkg.test_states[i] {
		case .Ready:
			continue
		case .Running:
			highest_run_index = max(highest_run_index, i)
		case .Successful:
			done_count += 1
		case .Failed:
			failed_count += 1
			done_count += 1
		}
	}

	start := max(0, highest_run_index - (report.progress_width - 1))
	end   := min(start + report.progress_width, len(pkg.test_states))

	// This variable is to keep track of the last ANSI code emitted, in
	// order to avoid repeating the same code over in a sequence.
	//
	// This should help reduce screen flicker.
	last_state := Test_State(-1)

	io.write_string(line_writer, pkg.header)

	#no_bounds_check for state in pkg.test_states[start:end] {
		switch state {
		case .Ready:
			if last_state != state {
				io.write_string(line_writer, SGR_READY)
				last_state = state
			}
		case .Running:
			if last_state != state {
				io.write_string(line_writer, SGR_RUNNING)
				last_state = state
			}
		case .Successful:
			if last_state != state {
				io.write_string(line_writer, SGR_SUCCESS)
				last_state = state
			}
		case .Failed:
			if last_state != state {
				io.write_string(line_writer, SGR_FAILED)
				last_state = state
			}
		}
		io.write_byte(line_writer, '|')
	}

	for _ in 0 ..< report.progress_width - (end - start) {
		io.write_byte(line_writer, ' ')
	}

	io.write_string(line_writer, SGR_RESET + "] ")

	ticker: string
	if done_count == len(pkg.test_states) {
		ticker = "[package done]"
		if failed_count > 0 {
			ticker = fmt.tprintf("%s (" + SGR_FAILED + "%i" + SGR_RESET + " failed)", ticker, failed_count)
		}
	} else {
		if len(pkg.last_change_name) == 0 {
			#no_bounds_check pkg.last_change_name = pkg.tests[0].name
		}

		switch pkg.last_change_state {
		case .Ready:
			ticker = fmt.tprintf(SGR_READY + "%s" + SGR_RESET, pkg.last_change_name)
		case .Running:
			ticker = fmt.tprintf(SGR_RUNNING + "%s" + SGR_RESET, pkg.last_change_name)
		case .Failed:
			ticker = fmt.tprintf(SGR_FAILED + "%s" + SGR_RESET, pkg.last_change_name)
		case .Successful:
			ticker = fmt.tprintf(SGR_SUCCESS + "%s" + SGR_RESET, pkg.last_change_name)
		}
	}

	if done_count == len(pkg.test_states) {
		fmt.wprintfln(line_writer, "     % 4i :: %s",
			len(pkg.test_states),
			ticker,
		)
	} else {
		fmt.wprintfln(line_writer, "% 4i/% 4i :: %s",
			done_count,
			len(pkg.test_states),
			ticker,
		)
	}

	pkg.redraw_string = strings.to_string(line_builder)
	pkg.frame_ready = true
	io.write_string(w, pkg.redraw_string)
}

redraw_report :: proc(w: io.Writer, report: Report) {
	// If we print a line longer than the user's terminal can handle, it may
	// wrap around, shifting the progress report out of alignment.
	//
	// There are ways to get the current terminal width, and that would be the
	// ideal way to handle this, but it would require system-specific code such
	// as setting STDIN to be non-blocking in order to read the response from
	// the ANSI DSR escape code, or reading environment variables.
	//
	// The DECAWM escape codes control whether or not the terminal will wrap
	// long lines or overwrite the last visible character.
	// This should be fine for now.
	//
	// Note that we only do this for the animated summary; log messages are
	// still perfectly fine to wrap, as they're printed in their own batch,
	// whereas the animation depends on each package being only on one line.
	//
	// Of course, if you resize your terminal while it's printing, things can
	// still break...
	fmt.wprint(w, ansi.CSI + ansi.DECAWM_OFF)
	for &pkg in report.packages {
		redraw_package(w, report, &pkg)
	}
	fmt.wprint(w, ansi.CSI + ansi.DECAWM_ON)
}

needs_to_redraw :: proc(report: Report) -> bool {
	for pkg in report.packages {
		if !pkg.frame_ready {
			return true
		}
	}

	return false
}

draw_status_bar :: proc(w: io.Writer, threads_string: string, total_done_count, total_test_count: int) {
	if total_done_count == total_test_count {
		// All tests are done; print a blank line to maintain the same height
		// of the progress report.
		fmt.wprintln(w)
	} else {
		fmt.wprintfln(w,
			"%s % 4i/% 4i :: total",
			threads_string,
			total_done_count,
			total_test_count)
	}
}

write_memory_report :: proc(w: io.Writer, tracker: ^mem.Tracking_Allocator, pkg, name: string) {
	fmt.wprintf(w,
		"<% 10M/% 10M> <% 10M> (% 5i/% 5i) :: %s.%s",
		tracker.current_memory_allocated,
		tracker.total_memory_allocated,
		tracker.peak_memory_allocated,
		tracker.total_free_count,
		tracker.total_allocation_count,
		pkg,
		name)

	for ptr, entry in tracker.allocation_map {
		fmt.wprintf(w,
			"\n        +++ leak % 10M @ %p [%s:%i:%s()]",
			entry.size,
			ptr,
			filepath.base(entry.location.file_path),
			entry.location.line,
			entry.location.procedure)
		}

	for entry in tracker.bad_free_array {
		fmt.wprintf(w,
			"\n        +++ bad free        @ %p [%s:%i:%s()]",
			entry.memory,
			filepath.base(entry.location.file_path),
			entry.location.line,
			entry.location.procedure)
	}
}
