/*
 * This file is part of the coreinfo project.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#include "coreinfo.h"
#include <commonlib/timestamp_serialized.h>

#if IS_ENABLED(CONFIG_MODULE_TIMESTAMPS)

#define LINES_SHOWN 19
#define TAB_WIDTH 2

/* Globals that are used for tracking screen state */
static char *g_buf;
static s32 g_line;
static s32 g_lines_count;
static s32 g_max_cursor_line;

static unsigned long tick_freq_mhz;

static const char *timestamp_name(uint32_t id)
{
	int i;

	for (i = 0; i < ARRAY_SIZE(timestamp_ids); i++) {
		if (timestamp_ids[i].id == id)
			return timestamp_ids[i].name;
	}

	return "<unknown>";
}

static void timestamp_set_tick_freq(unsigned long table_tick_freq_mhz)
{
	tick_freq_mhz = table_tick_freq_mhz;

	/* Honor table frequency. */
	if (tick_freq_mhz)
		return;

	tick_freq_mhz = lib_sysinfo.cpu_khz / 1000;

	if (!tick_freq_mhz) {
		fprintf(stderr, "Cannot determine timestamp tick frequency.\n");
		exit(1);
	}
}

static u64 arch_convert_raw_ts_entry(u64 ts)
{
	return ts / tick_freq_mhz;
}

static u32 char_width(char c, u32 cursor, u32 screen_width)
{
	if (c == '\n')
		return screen_width - (cursor % screen_width);
	else if (c == '\t')
		return TAB_WIDTH;
	else if (isprint(c))
		return 1;

	return 0;
}

static u32 calculate_chars_count(char *str, u32 str_len, u32 screen_width,
		u32 screen_height)
{
	u32 i, count = 0;

	for (i = 0; i < str_len; i++)
		count += char_width(str[i], count, screen_width);

	/* Ensure that 'count' can occupy at least the whole screen */
	if (count < screen_width * screen_height)
		count = screen_width * screen_height;

	/* Pad to line end */
	if (count % screen_width != 0)
		count += screen_width - (count % screen_width);

	return count;
}

/*
 * This method takes an input buffer and sanitizes it for display, which means:
 *  - '\n' is converted to spaces until end of line
 *  - Tabs are converted to spaces of size TAB_WIDTH
 *  - Only printable characters are preserved
 */
static int sanitize_buffer_for_display(char *str, u32 str_len, char *out,
		u32 out_len, u32 screen_width)
{
	u32 cursor = 0;
	u32 i;

	for (i = 0; i < str_len && cursor < out_len; i++) {
		u32 width = char_width(str[i], cursor, screen_width);

		if (width == 1)
			out[cursor++] = str[i];
		else if (width > 1)
			while (width-- && cursor < out_len)
				out[cursor++] = ' ';
	}

	/* Fill the rest of the out buffer with spaces */
	while (cursor < out_len)
		out[cursor++] = ' ';

	return 0;
}

static uint64_t timestamp_print_entry(char *buffer, size_t size, uint32_t *cur,
		uint32_t id, uint64_t stamp, uint64_t prev_stamp)
{
	const char *name;
	uint64_t step_time;

	name = timestamp_name(id);
	step_time = arch_convert_raw_ts_entry(stamp - prev_stamp);

	*cur += snprintf(buffer + *cur, size, "%4d: %-45s", id, name);
	*cur += snprintf(buffer + *cur, size, "%llu",
			arch_convert_raw_ts_entry(stamp));
	if (prev_stamp) {
		*cur += snprintf(buffer + *cur, size, " (");
		*cur += snprintf(buffer + *cur, size, "%llu", step_time);
		*cur += snprintf(buffer + *cur, size, ")");
	}
	*cur += snprintf(buffer + *cur, size, "\n");

	return step_time;
}

static int timestamps_module_init(void)
{
	/* Make sure that lib_sysinfo is initialized */
	int ret = lib_get_sysinfo();

	if (ret)
		return -1;

	struct timestamp_table *timestamps = lib_sysinfo.tstamp_table;

	if (timestamps == NULL)
		return -1;

	/* Extract timestamps information */
	u64 base_time = timestamps->base_time;
	u16 max_entries = timestamps->max_entries;
	u32 n_entries = timestamps->num_entries;

	timestamp_set_tick_freq(timestamps->tick_freq_mhz);

	char *buffer;
	u32 buff_cur = 0;
	uint64_t prev_stamp;
	uint64_t total_time;

	/* Allocate a buffer big enough to contain all of the possible
	 * entries plus the other information (number entries, total time). */
	buffer = malloc((max_entries + 4) * SCREEN_X * sizeof(char));

	if (buffer == NULL)
		return -3;

	/* Write the content */
	buff_cur += snprintf(buffer, SCREEN_X, "%d entries total:\n\n",
			n_entries);

	prev_stamp = 0;
	timestamp_print_entry(buffer, SCREEN_X, &buff_cur, 0, base_time,
			prev_stamp);
	prev_stamp = base_time;

	total_time = 0;
	for (int i = 0; i < n_entries; i++) {
		uint64_t stamp;
		const struct timestamp_entry *tse = &timestamps->entries[i];

		stamp = tse->entry_stamp + base_time;
		total_time += timestamp_print_entry(buffer, SCREEN_X,
				&buff_cur, tse->entry_id, stamp, prev_stamp);
		prev_stamp = stamp;
	}

	buff_cur += snprintf(buffer + buff_cur, SCREEN_X, "\nTotal Time: ");
	buff_cur += snprintf(buffer + buff_cur, SCREEN_X, "%llu", total_time);
	buff_cur += snprintf(buffer + buff_cur, SCREEN_X, "\n");

	/* Calculate how many characters will be displayed on screen */
	u32 chars_count = calculate_chars_count(buffer, buff_cur + 1,
			SCREEN_X, LINES_SHOWN);

	/* Sanity check, chars_count must be padded to full line */
	if (chars_count % SCREEN_X != 0)
		return -2;

	g_lines_count = chars_count / SCREEN_X;
	g_max_cursor_line = MAX(g_lines_count - 1 - LINES_SHOWN, 0);

	g_buf = malloc(chars_count);
	if (!g_buf) {
		free(buffer);
		return -3;
	}

	if (sanitize_buffer_for_display(buffer, buff_cur + 1, g_buf,
				chars_count, SCREEN_X) < 0) {
		free(buffer);
		free(g_buf);
		g_buf = NULL;
		return -4;
	}

	free(buffer);

	return 0;
}

static int timestamps_module_redraw(WINDOW *win)
{
	print_module_title(win, "coreboot Timestamps");

	if (!g_buf)
		return -1;

	int x = 0, y = 0;
	char *tmp = g_buf + g_line * SCREEN_X;

	for (y = 0; y < LINES_SHOWN; y++) {
		for (x = 0; x < SCREEN_X; x++) {
			mvwaddch(win, y + 2, x, *tmp);
			tmp++;
		}
	}

	return 0;
}

static int timestamps_module_handle(int key)
{
	if (!g_buf)
		return 0;

	switch (key) {
	case KEY_DOWN:
		g_line++;
		break;
	case KEY_UP:
		g_line--;
		break;
	case KEY_NPAGE: /* Page up */
		g_line -= LINES_SHOWN;
		break;
	case KEY_PPAGE: /* Page down */
		g_line += LINES_SHOWN;
		break;
	}

	if (g_line < 0)
		g_line = 0;

	if (g_line > g_max_cursor_line)
		g_line = g_max_cursor_line;

	return 1;
}

struct coreinfo_module timestamps_module = {
	.name = "Timestamps",
	.init = timestamps_module_init,
	.redraw = timestamps_module_redraw,
	.handle = timestamps_module_handle,
};

#else

struct coreinfo_module timestamps_module = {
};

#endif