// SPDX-License-Identifier: BSD-2-Clause-Patent
/*
 * Copyright 2012 <James.Bottomley@HansenPartnership.com>
 * Copyright 2013 Red Hat Inc. <pjones@redhat.com>
 */
#include "shim.h"

static UINT8 console_text_mode = 0;

static int
count_lines(CHAR16 *str_arr[])
{
	int i = 0;

	while (str_arr[i])
		i++;
	return i;
}

static void
SetMem16(CHAR16 *dst, UINT32 n, CHAR16 c)
{
	unsigned int i;

	for (i = 0; i < n/2; i++) {
		dst[i] = c;
	}
}

EFI_STATUS
console_get_keystroke(EFI_INPUT_KEY *key)
{
	SIMPLE_INPUT_INTERFACE *ci = ST->ConIn;
	UINTN EventIndex;
	EFI_STATUS efi_status;

	if (!ci)
		return EFI_UNSUPPORTED;

	do {
		BS->WaitForEvent(1, &ci->WaitForKey, &EventIndex);
		efi_status = ci->ReadKeyStroke(ci, key);
	} while (efi_status == EFI_NOT_READY);

	return efi_status;
}

static VOID setup_console (int text)
{
	EFI_STATUS efi_status;
	EFI_CONSOLE_CONTROL_PROTOCOL *concon;
	static EFI_CONSOLE_CONTROL_SCREEN_MODE mode =
					EfiConsoleControlScreenGraphics;
	EFI_CONSOLE_CONTROL_SCREEN_MODE new_mode;

	efi_status = LibLocateProtocol(&EFI_CONSOLE_CONTROL_GUID,
				       (VOID **)&concon);
	if (EFI_ERROR(efi_status))
		return;

	if (text) {
		new_mode = EfiConsoleControlScreenText;

		efi_status = concon->GetMode(concon, &mode, 0, 0);
		/* If that didn't work, assume it's graphics */
		if (EFI_ERROR(efi_status))
			mode = EfiConsoleControlScreenGraphics;
		if (text < 0) {
			if (mode == EfiConsoleControlScreenGraphics)
				console_text_mode = 0;
			else
				console_text_mode = 1;
			return;
		}
	} else {
		new_mode = mode;
	}

	concon->SetMode(concon, new_mode);
	console_text_mode = text;
}

VOID console_fini(VOID)
{
	if (console_text_mode)
		setup_console(0);
}

UINTN EFIAPI
console_print(const CHAR16 *fmt, ...)
{
	ms_va_list args;
	UINTN ret;

	if (!console_text_mode)
		setup_console(1);

	ms_va_start(args, fmt);
	ret = VPrint(fmt, args);
	ms_va_end(args);

	return ret;
}

UINTN EFIAPI
console_print_at(UINTN col, UINTN row, const CHAR16 *fmt, ...)
{
	SIMPLE_TEXT_OUTPUT_INTERFACE *co = ST->ConOut;
	ms_va_list args;
	UINTN ret;

	if (!console_text_mode)
		setup_console(1);

	if (co)
		co->SetCursorPosition(co, col, row);

	ms_va_start(args, fmt);
	ret = VPrint(fmt, args);
	ms_va_end(args);

	return ret;
}

static struct {
	CHAR16 up_left;
	CHAR16 up_right;
	CHAR16 down_left;
	CHAR16 down_right;
	CHAR16 horizontal;
	CHAR16 vertical;
} boxdraw[2] = {
	{
		BOXDRAW_UP_LEFT,
		BOXDRAW_UP_RIGHT,
		BOXDRAW_DOWN_LEFT,
		BOXDRAW_DOWN_RIGHT,
		BOXDRAW_HORIZONTAL,
		BOXDRAW_VERTICAL
	}, {
		'+',
		'+',
		'+',
		'+',
		'-',
		'|'
	}
};

void
console_print_box_at(CHAR16 *str_arr[], int highlight,
		     int start_col, int start_row,
		     int size_cols, int size_rows,
		     int offset, int lines)
{
	int i;
	SIMPLE_TEXT_OUTPUT_INTERFACE *co = ST->ConOut;
	UINTN rows, cols;
	CHAR16 *Line;
	bool char_set;

	if (lines == 0)
		return;

	if (!console_text_mode)
		setup_console(1);

	if (!co)
		return;

	co->QueryMode(co, co->Mode->Mode, &cols, &rows);

	/* last row on screen is unusable without scrolling, so ignore it */
	rows--;

	if (size_rows < 0)
		size_rows = rows + size_rows + 1;
	if (size_cols < 0)
		size_cols = cols + size_cols + 1;

	if (start_col < 0)
		start_col = (cols + start_col + 2)/2;
	if (start_row < 0)
		start_row = (rows + start_row + 2)/2;
	if (start_col < 0)
		start_col = 0;
	if (start_row < 0)
		start_row = 0;

	if (start_col > (int)cols || start_row > (int)rows) {
		console_print(L"Starting Position (%d,%d) is off screen\n",
			      start_col, start_row);
		return;
	}
	if (size_cols + start_col > (int)cols)
		size_cols = cols - start_col;
	if (size_rows + start_row > (int)rows)
		size_rows = rows - start_row;

	if (lines > size_rows - 2)
		lines = size_rows - 2;

	Line = AllocatePool((size_cols+1)*sizeof(CHAR16));
	if (!Line) {
		console_print(L"Failed Allocation\n");
		return;
	}

	/* test if boxdraw characters work */
	co->SetCursorPosition(co, start_col, start_row);
	Line[0] = boxdraw[0].up_left;
	Line[1] = L'\0';
	char_set = co->OutputString(co, Line) == 0 ? 0 : 1;

	SetMem16 (Line, size_cols * 2, boxdraw[char_set].horizontal);

	Line[0] = boxdraw[char_set].down_right;
	Line[size_cols - 1] = boxdraw[char_set].down_left;
	Line[size_cols] = L'\0';
	co->SetCursorPosition(co, start_col, start_row);
	co->OutputString(co, Line);

	int start;
	if (offset == 0)
		/* middle */
		start = (size_rows - lines)/2 + start_row + offset;
	else if (offset < 0)
		/* from bottom */
		start = start_row + size_rows - lines + offset - 1;
	else
		/* from top */
		start = start_row + offset;

	for (i = start_row + 1; i < size_rows + start_row - 1; i++) {
		int line = i - start;

		SetMem16 (Line, size_cols*2, L' ');
		Line[0] = boxdraw[char_set].vertical;
		Line[size_cols - 1] = boxdraw[char_set].vertical;
		Line[size_cols] = L'\0';
		if (line >= 0 && line < lines) {
			CHAR16 *s = str_arr[line];
			int len = StrLen(s);
			int col = (size_cols - 2 - len)/2;

			if (col < 0)
				col = 0;

			CopyMem(Line + col + 1, s, MIN(len, size_cols - 2)*2);
		}
		if (line >= 0 && line == highlight)
			co->SetAttribute(co, EFI_LIGHTGRAY |
					       EFI_BACKGROUND_BLACK);
		co->SetCursorPosition(co, start_col, i);
		co->OutputString(co, Line);
		if (line >= 0 && line == highlight)
			co->SetAttribute(co, EFI_LIGHTGRAY |
					       EFI_BACKGROUND_BLUE);

	}
	SetMem16 (Line, size_cols * 2, boxdraw[char_set].horizontal);
	Line[0] = boxdraw[char_set].up_right;
	Line[size_cols - 1] = boxdraw[char_set].up_left;
	Line[size_cols] = L'\0';
	co->SetCursorPosition(co, start_col, i);
	co->OutputString(co, Line);

	FreePool (Line);

}

void
console_print_box(CHAR16 *str_arr[], int highlight)
{
	SIMPLE_TEXT_OUTPUT_MODE SavedConsoleMode;
	SIMPLE_TEXT_OUTPUT_INTERFACE *co = ST->ConOut;
	EFI_INPUT_KEY key;

	if (!console_text_mode)
		setup_console(1);

	if (!co)
		return;

	CopyMem(&SavedConsoleMode, co->Mode, sizeof(SavedConsoleMode));
	co->EnableCursor(co, FALSE);
	co->SetAttribute(co, EFI_LIGHTGRAY | EFI_BACKGROUND_BLUE);

	console_print_box_at(str_arr, highlight, 0, 0, -1, -1, 0,
			     count_lines(str_arr));

	console_get_keystroke(&key);

	co->EnableCursor(co, SavedConsoleMode.CursorVisible);
	co->SetCursorPosition(co, SavedConsoleMode.CursorColumn,
				SavedConsoleMode.CursorRow);
	co->SetAttribute(co, SavedConsoleMode.Attribute);
}

int
console_select(CHAR16 *title[], CHAR16* selectors[], unsigned int start)
{
	SIMPLE_TEXT_OUTPUT_MODE SavedConsoleMode;
	SIMPLE_TEXT_OUTPUT_INTERFACE *co = ST->ConOut;
	EFI_INPUT_KEY k;
	EFI_STATUS efi_status;
	int selector;
	unsigned int selector_lines = count_lines(selectors);
	int selector_max_cols = 0;
	unsigned int i;
	int offs_col, offs_row, size_cols, size_rows, lines;
	unsigned int selector_offset;
	UINTN cols, rows;

	if (!console_text_mode)
		setup_console(1);

	if (!co)
		return -1;

	co->QueryMode(co, co->Mode->Mode, &cols, &rows);

	for (i = 0; i < selector_lines; i++) {
		int len = StrLen(selectors[i]);

		if (len > selector_max_cols)
			selector_max_cols = len;
	}

	if (start >= selector_lines)
		start = selector_lines - 1;

	offs_col = - selector_max_cols - 4;
	size_cols = selector_max_cols + 4;

	if (selector_lines > rows - 10) {
		int title_lines = count_lines(title);
		offs_row =  title_lines + 1;
		size_rows = rows - 3 - title_lines;
		lines = size_rows - 2;
	} else {
		offs_row = - selector_lines - 4;
		size_rows = selector_lines + 2;
		lines = selector_lines;
	}

	if (start > (unsigned)lines) {
		selector = lines;
		selector_offset = start - lines;
	} else {
		selector = start;
		selector_offset = 0;
	}

	CopyMem(&SavedConsoleMode, co->Mode, sizeof(SavedConsoleMode));
	co->EnableCursor(co, FALSE);
	co->SetAttribute(co, EFI_LIGHTGRAY | EFI_BACKGROUND_BLUE);

	console_print_box_at(title, -1, 0, 0, -1, -1, 1, count_lines(title));

	console_print_box_at(selectors, selector, offs_col, offs_row,
			     size_cols, size_rows, 0, lines);

	do {
		efi_status = console_get_keystroke(&k);
		if (EFI_ERROR (efi_status)) {
			console_print(L"Failed to read the keystroke: %r",
				      efi_status);
			selector = -1;
			break;
		}

		if (k.ScanCode == SCAN_ESC) {
			selector = -1;
			break;
		}

		if (k.ScanCode == SCAN_UP) {
			if (selector > 0)
				selector--;
			else if (selector_offset > 0)
				selector_offset--;
		} else if (k.ScanCode == SCAN_DOWN) {
			if (selector < lines - 1)
				selector++;
			else if (selector_offset < (selector_lines - lines))
				selector_offset++;
		}

		console_print_box_at(&selectors[selector_offset], selector,
				     offs_col, offs_row,
				     size_cols, size_rows, 0, lines);
	} while (!(k.ScanCode == SCAN_NULL
		   && k.UnicodeChar == CHAR_CARRIAGE_RETURN));

	co->EnableCursor(co, SavedConsoleMode.CursorVisible);
	co->SetCursorPosition(co, SavedConsoleMode.CursorColumn,
			      SavedConsoleMode.CursorRow);
	co->SetAttribute(co, SavedConsoleMode.Attribute);

	if (selector < 0)
		/* ESC pressed */
		return selector;
	return selector + selector_offset;
}


int
console_yes_no(CHAR16 *str_arr[])
{
	CHAR16 *yes_no[] = { L"No", L"Yes", NULL };
	return console_select(str_arr, yes_no, 0);
}

void
console_alertbox(CHAR16 **title)
{
	CHAR16 *okay[] = { L"OK", NULL };
	console_select(title, okay, 0);
}

void
console_errorbox(CHAR16 *err)
{
	CHAR16 **err_arr = (CHAR16 *[]){
		L"ERROR",
		L"",
		0,
		0,
	};

	err_arr[2] = err;

	console_alertbox(err_arr);
}

void
console_notify(CHAR16 *string)
{
	CHAR16 **str_arr = (CHAR16 *[]){
		0,
		0,
	};

	str_arr[0] = string;

	console_alertbox(str_arr);
}

void
console_save_and_set_mode(SIMPLE_TEXT_OUTPUT_MODE * SavedMode)
{
	SIMPLE_TEXT_OUTPUT_INTERFACE *co = ST->ConOut;

	if (!SavedMode) {
		console_print(L"Invalid parameter: SavedMode\n");
		return;
	}

	if (!co)
		return;

	CopyMem(SavedMode, co->Mode, sizeof(SIMPLE_TEXT_OUTPUT_MODE));
	co->EnableCursor(co, FALSE);
	co->SetAttribute(co, EFI_LIGHTGRAY | EFI_BACKGROUND_BLUE);
}

void
console_restore_mode(SIMPLE_TEXT_OUTPUT_MODE * SavedMode)
{
	SIMPLE_TEXT_OUTPUT_INTERFACE *co = ST->ConOut;

	if (!co)
		return;

	co->EnableCursor(co, SavedMode->CursorVisible);
	co->SetCursorPosition(co, SavedMode->CursorColumn,
				SavedMode->CursorRow);
	co->SetAttribute(co, SavedMode->Attribute);
}

int
console_countdown(CHAR16* title, const CHAR16* message, int timeout)
{
	SIMPLE_TEXT_OUTPUT_INTERFACE *co = ST->ConOut;
	SIMPLE_INPUT_INTERFACE *ci = ST->ConIn;
	SIMPLE_TEXT_OUTPUT_MODE SavedMode;
	EFI_INPUT_KEY key;
	EFI_STATUS efi_status;
	UINTN cols, rows;
	CHAR16 *titles[2];
	int wait = 10000000;

	if (!co || !ci)
		return -1;

	console_save_and_set_mode(&SavedMode);

	titles[0] = title;
	titles[1] = NULL;

	console_print_box_at(titles, -1, 0, 0, -1, -1, 1, 1);

	co->QueryMode(co, co->Mode->Mode, &cols, &rows);

	console_print_at((cols - StrLen(message)) / 2, rows / 2, message);
	while (1) {
		if (timeout > 1)
			console_print_at(2, rows - 3,
					 L"Booting in %d seconds  ",
					 timeout);
		else if (timeout)
			console_print_at(2, rows - 3,
					 L"Booting in %d second   ",
					 timeout);

		efi_status = WaitForSingleEvent(ci->WaitForKey, wait);
		if (efi_status != EFI_TIMEOUT) {
			/* Clear the key in the queue */
			ci->ReadKeyStroke(ci, &key);
			break;
		}

		timeout--;
		if (!timeout)
			break;
	}

	console_restore_mode(&SavedMode);

	return timeout;
}

#define HORIZONTAL_MAX_OK 1920
#define VERTICAL_MAX_OK 1080
#define COLUMNS_MAX_OK 200
#define ROWS_MAX_OK 100

void
console_mode_handle(VOID)
{
	SIMPLE_TEXT_OUTPUT_INTERFACE *co = ST->ConOut;
	EFI_GRAPHICS_OUTPUT_PROTOCOL *gop;
	EFI_GUID gop_guid = EFI_GRAPHICS_OUTPUT_PROTOCOL_GUID;
	EFI_GRAPHICS_OUTPUT_MODE_INFORMATION *Info;

	UINTN mode_set;
	UINTN rows = 0, columns = 0;
	EFI_STATUS efi_status = EFI_SUCCESS;

	if (!co)
		return;

	efi_status = BS->LocateProtocol(&gop_guid, NULL, (void **)&gop);
	if (EFI_ERROR(efi_status)) {
		return;
	}

	Info = gop->Mode->Info;

	/*
	 * Start verifying if we are in a resolution larger than Full HD
	 * (1920x1080). If we're not, assume we're in a good mode and do not
	 * try to change it.
	 */
	if (Info->HorizontalResolution <= HORIZONTAL_MAX_OK &&
	    Info->VerticalResolution <= VERTICAL_MAX_OK) {
		/* keep original mode and return */
		return;
	}

        efi_status = co->QueryMode(co, co->Mode->Mode, &columns, &rows);
	if (EFI_ERROR(efi_status)) {
		console_error(L"Console query mode fail", efi_status);
		return;
	}

	/*
	 * Verify current console output to check if the character columns and
	 * rows in a good mode.
	 */
	if (columns <= COLUMNS_MAX_OK && rows <= ROWS_MAX_OK) {
		/* keep original mode and return */
		return;
	}

	if (!console_text_mode)
		setup_console(1);

	co->Reset(co, TRUE);

	/*
	 * If we reached here, then we have a high resolution screen and the
	 * text too small. Try to switch to a better mode. Mode number 2 is
	 * first non standard mode, which is provided by the device
	 * manufacturer, so it should be a good mode.
	 */
	if (co->Mode->MaxMode > 2)
		mode_set = 2;
	else
		mode_set = 0;

	efi_status = co->SetMode(co, mode_set);
	if (EFI_ERROR(efi_status) && mode_set != 0) {
		/*
		 * Set to 0 mode which is required that all output devices
		 * support at least 80x25 text mode.
		 */
		mode_set = 0;
		efi_status = co->SetMode(co, mode_set);
	}

	clear_screen();

	if (EFI_ERROR(efi_status)) {
		console_error(L"Console set mode fail", efi_status);
	}

	return;
}

/* Copy of gnu-efi-3.0 with the added secure boot strings */
static struct {
    EFI_STATUS      Code;
    CHAR16	   *Desc;
} error_table[] = {
	{  EFI_SUCCESS,                L"Success"},
	{  EFI_LOAD_ERROR,             L"Load Error"},
	{  EFI_INVALID_PARAMETER,      L"Invalid Parameter"},
	{  EFI_UNSUPPORTED,            L"Unsupported"},
	{  EFI_BAD_BUFFER_SIZE,        L"Bad Buffer Size"},
	{  EFI_BUFFER_TOO_SMALL,       L"Buffer Too Small"},
	{  EFI_NOT_READY,              L"Not Ready"},
	{  EFI_DEVICE_ERROR,           L"Device Error"},
	{  EFI_WRITE_PROTECTED,        L"Write Protected"},
	{  EFI_OUT_OF_RESOURCES,       L"Out of Resources"},
	{  EFI_VOLUME_CORRUPTED,       L"Volume Corrupt"},
	{  EFI_VOLUME_FULL,            L"Volume Full"},
	{  EFI_NO_MEDIA,               L"No Media"},
	{  EFI_MEDIA_CHANGED,          L"Media changed"},
	{  EFI_NOT_FOUND,              L"Not Found"},
	{  EFI_ACCESS_DENIED,          L"Access Denied"},
	{  EFI_NO_RESPONSE,            L"No Response"},
	{  EFI_NO_MAPPING,             L"No mapping"},
	{  EFI_TIMEOUT,                L"Time out"},
	{  EFI_NOT_STARTED,            L"Not started"},
	{  EFI_ALREADY_STARTED,        L"Already started"},
	{  EFI_ABORTED,                L"Aborted"},
	{  EFI_ICMP_ERROR,             L"ICMP Error"},
	{  EFI_TFTP_ERROR,             L"TFTP Error"},
	{  EFI_PROTOCOL_ERROR,         L"Protocol Error"},
	{  EFI_INCOMPATIBLE_VERSION,   L"Incompatible Version"},
	{  EFI_SECURITY_VIOLATION,     L"Security Violation"},
	{  EFI_HTTP_ERROR,             L"HTTP Error"},

	// warnings
	{  EFI_WARN_UNKNOWN_GLYPH,     L"Warning Unknown Glyph"},
	{  EFI_WARN_DELETE_FAILURE,    L"Warning Delete Failure"},
	{  EFI_WARN_WRITE_FAILURE,     L"Warning Write Failure"},
	{  EFI_WARN_BUFFER_TOO_SMALL,  L"Warning Buffer Too Small"},
	{  0, NULL}
} ;


static CHAR16 *
err_string (
    IN EFI_STATUS       efi_status
    )
{
	UINTN           Index;

	for (Index = 0; error_table[Index].Desc; Index +=1) {
		if (error_table[Index].Code == efi_status) {
			return error_table[Index].Desc;
		}
	}

	return L"";
}

void
console_error(CHAR16 *err, EFI_STATUS efi_status)
{
	CHAR16 **err_arr = (CHAR16 *[]){
		L"ERROR",
		L"",
		0,
		0,
	};
	CHAR16 str[512];

	SPrint(str, sizeof(str), L"%s: (0x%x) %s", err, efi_status,
	       err_string(efi_status));

	err_arr[2] = str;

	console_alertbox(err_arr);
}

void
console_reset(void)
{
	SIMPLE_TEXT_OUTPUT_INTERFACE *co = ST->ConOut;

	if (!console_text_mode)
		setup_console(1);

	if (!co)
		return;

	co->Reset(co, TRUE);
	/* set mode 0 - required to be 80x25 */
	co->SetMode(co, 0);
	co->ClearScreen(co);
}

void
clear_screen(void)
{
	SIMPLE_TEXT_OUTPUT_INTERFACE *co = ST->ConOut;

	if (!co)
		return;

	co->ClearScreen(co);
}

VOID
setup_verbosity(VOID)
{
	EFI_STATUS efi_status;
	UINT8 *verbose_check_ptr = NULL;
	UINTN verbose_check_size;

	verbose_check_size = sizeof(verbose);
	efi_status = get_variable(VERBOSE_VAR_NAME, &verbose_check_ptr,
				  &verbose_check_size, SHIM_LOCK_GUID);
	if (!EFI_ERROR(efi_status)) {
		verbose = *(__typeof__(verbose) *)verbose_check_ptr;
		verbose &= (1ULL << (8 * verbose_check_size)) - 1ULL;
		FreePool(verbose_check_ptr);
	}

	setup_console(-1);
}

#ifndef SHIM_UNIT_TEST
VOID
usleep(unsigned long usecs)
{
	BS->Stall(usecs);
}
#endif

/* This is used in various things to determine if we should print to the
 * console */
UINT8 in_protocol = 0;
