/*
 * Copyright (C) Volition, Inc. 1999.  All rights reserved.
 *
 * All source code herein is the property of Volition, Inc. You may not sell 
 * or otherwise commercially exploit the source or things you created based on the 
 * source.
 *
*/ 


#include "debugconsole/console.h"
#include "globalincs/alphacolors.h"
#include "globalincs/version.h"
#include "graphics/2d.h"
#include "graphics/font.h"
#include "io/key.h"
#include "io/timer.h"
#include "osapi/osapi.h"

#include <algorithm>
#include <cmath>

// ========================= GLOBALS =========================
bool Dc_debug_on;		//!< Flag used to print console and command debugging strings

// Commands and History
SCP_string dc_command_str;		//!< The entered command line, arguments and all.
								//!< Is progressively culled from the left as commands, arguments are parsed in DCF's

// Misc
bool debug_inited = FALSE;
uint lastline = 0;  // Number of lines written to the console since the last command was processed


// ========================= LOCALS ==========================
// Text Buffer
uint DBROWS = 80;   // # of buffer rows
uint DBCOLS = 80;   // # of buffer columns
size_t lastwhite = 0; // Last whitespace character encountered, used by putc for 'true' word wrapping
ubyte DTABS = 4;    //!< Tab size in spaces

/**
 * Human readable versions of the dc_token's. Primarily used in error diagnosis
 */
static
const char *token_str[DCT_MAX_ITEMS] =
{
	"nothing",
	"string",
	"float",
	"integer",
	"unsigned integer",
	"byte",
	"unsigned byte",
	"boolean"
};

SCP_deque<SCP_string> dc_buffer;

// Display Window
uint DROWS = 25;
uint DCOLS = 80;
const uint DROWS_MIN = 25;
const uint DCOLS_MIN = 80;
uint dc_scroll_x;   // X scroll position (Leftmost character)
uint dc_scroll_y;   // Y scroll position (Topmost character)
int row_height;     // Row/Line height, in pixels
int col_width;      // Col/Character width, in pixels
int dc_font = font::FONT1;

SCP_string dc_title;

#define SCROLL_Y_MAX (DBROWS - DROWS)

// Commands and History
uint DCMDS = 40;			// Max number of commands to remember

SCP_deque<SCP_string> dc_history;
SCP_deque<SCP_string>::iterator last_oldcommand;		// Iterator to the last old command. Is reset to the start every new command push.

const char dc_prompt[]= "> ";	// The prompt c_str
SCP_string dc_command_buf;		// The command line as shown in the console. Essentially an input buffer for dc_command_str

// Local functions
/**
 * @brief Initializes the debug console.
 */
void dc_init(void);

/**
 * @brief Process the entered command string
 */
void dc_do_command(SCP_string *cmd_str);

/**
 * @brief Draws the in-game console.
 */
void dc_draw(bool show_prompt);

/**
 * Draws the cursor
 * @param [in] cmd_string	The formatted command string displayed by dc_draw_window
 * @param [in] x		The x screen position of the command string
 * @param [in] y		The y screen position of the command string
 */
void dc_draw_cursor( SCP_string &cmd_string, int x, int y );

/**
 * Draws the window text
 */
void dc_draw_window(bool show_prompt);

/**
 * @brief   Stuffs the given character into the output buffer.
 * @details Also handles tab alignment, newlines, and maintains the target.
 */
void dc_putc(char c);

// ============================== IMPLEMENTATIONS =============================
void dc_do_command(SCP_string *cmd_str)
{
	/**
	 * Grab the first word from the cmd_str
	 *  If it is not a literal, ignore it "Invalid keyword: %s"
	 *  Search for the command...
	 *      Compare the word against valid commands
	 *      If command not found, ignore it "Invalid or unknown command: %s"\
	 *  Process the command...
	 *      Call the function to process the command (the rest of the command line is in the parser)
	 *          Function takes care of long_help and status depending on the mode.
	 */
	int i;
	SCP_string command;
	extern debug_command* dc_commands[];	// z64: I don't like this extern here, at all. Nope nope nope.

	if (cmd_str->empty()) {
		return;
	}

	dc_parse_init(*cmd_str);

	dc_stuff_string_white(command);		// Grab the first token, presumably this is a command

	for (i = 0; i < dc_commands_size; ++i) {

		if (stricmp(dc_commands[i]->name, command.c_str()) == 0) {
			break;
		} // Else, continue
	}

	if (i == dc_commands_size) {
		dc_printf("Command not found: '%s'\n", command.c_str());
		return;
	} // Else, we found our command

	try {
		dc_commands[i]->func();	// Run the command!
	
	} catch (const errParseString& err) {
		dc_printf("Require string(s) not found: \n");
		for (uint j = 0; j < err.expected_tokens.size(); ++j) {
			dc_printf("%i: %s\n", j, err.expected_tokens[j].c_str());
		}

		dc_printf("Found '%s' instead\n", err.found_token.c_str());
	
	} catch (const errParse& err) {
		dc_printf("Invalid argument. Expected %s, found '%s'\n", token_str[err.expected_type], err.found_token.c_str());

	}

	// dc_maybe_stuff_string is vulnerable to overflow. Once the errParseOverflow throw class (or w/e) gets
	// implemented, this last command should be put into its own try{} catch{} block.
	if (dc_maybe_stuff_string(command)) {
		dc_printf( "Ignoring the unused command line tail '%s'\n", command.c_str() );
	}
}

void dc_draw(bool show_prompt = FALSE)
{
	gr_clear();
	font::set_font(dc_font);
	gr_set_color_fast( &Color_bright );
	int w;
	gr_get_string_size(&w, nullptr, dc_title.c_str());

	gr_string((gr_screen.clip_width - w) / 2, 3, dc_title.c_str(), GR_RESIZE_NONE );

	gr_set_color_fast( &Color_normal );

	dc_draw_window(show_prompt);

	gr_flip();
}

void dc_draw_cursor( SCP_string &cmd_string, int x, int y )
{
	int t;
	int w, h;	// gr_string width and height

	t = timer_get_fixed_seconds() / (F1_0/3);
	if ( t & 1 ) {
		gr_get_string_size( &w, &h, cmd_string.c_str() );

		w %= (DCOLS * col_width);
		//gr_string( w, debug_y*16, "_" );
		gr_rect(gr_screen.center_offset_x + (x + (w + 1)), gr_screen.center_offset_y + (y + (h + 1)), 2,
			fl2i(font::get_current_font()->getHeight()), GR_RESIZE_NONE);
	}
}

void dc_draw_window(bool show_prompt)
{
	size_t cmd_lines;               // Number of lines for the command string
	size_t buffer_lines;            // Number of lines from the buffer to draw
	size_t i;                       // The current row we're drawing
	size_t j;                       // The current row of the command string we're drawing
	SCP_string out_str;             // The command string + prompt character
	SCP_string::iterator str_it;    // Iterator to out_str

	out_str = dc_prompt + dc_command_buf;
	cmd_lines = (out_str.size() / DCOLS) + 1;
	if (show_prompt) {
		buffer_lines = DROWS - cmd_lines;
	} else {
		buffer_lines = DROWS;
	}

	// Ensure the window is not bigger than the buffer
	CLAMP(DROWS, DROWS_MIN, DBROWS);
	CLAMP(DCOLS, DCOLS_MIN, DBCOLS);

	// Ensure we don't scroll too far
	dc_scroll_x = MIN(dc_scroll_x, (DBCOLS - DCOLS));
	if (dc_buffer.size() >= buffer_lines) {
		dc_scroll_y = MIN(dc_scroll_y, (uint32_t)(dc_buffer.size() - buffer_lines));
	} else {
		dc_scroll_y = 0;	// Disallow vscroll until the buffer is larger than the window
	}

	// Draw the buffer strings
	for (i = 0; i < buffer_lines; ++i) {
		if ((i + dc_scroll_y) < dc_buffer.size()) {
			gr_string(gr_screen.center_offset_x, gr_screen.center_offset_y + (((int)i * row_height) + row_height), dc_buffer[i + dc_scroll_y].substr(dc_scroll_x).c_str(), GR_RESIZE_NONE);
		}
	}

	// Draw the command string w/ padding only if the prompt is active.
	if (show_prompt) {
		i += 1;		// 1 line between the output and the input text
		j = 0;
		gr_set_color_fast(&Color_bright);
		for (str_it = out_str.begin(); str_it < out_str.end(); ++str_it) {
			if (j == (DCOLS - 1)) {
				// Insert a newline char at every place the string needs to return the 'carriage'
				str_it = out_str.insert(str_it, '\n');
				j = 0;
			} else {
				++j;
			}
		}
		gr_string(gr_screen.center_offset_x, gr_screen.center_offset_y + (((int)i * row_height) + row_height), out_str.c_str(), GR_RESIZE_NONE);

		dc_draw_cursor(out_str, 0, (((int)i * row_height)));
		gr_set_color_fast(&Color_normal);
	}
}

void dc_init(void)
{
	if (debug_inited) {
		return;
	}

	debug_inited = TRUE;

	// Init window settings
	dc_font = font::FONT1;
	row_height = ((fl2i(font::get_current_font()->getHeight())) * 3) / 2;	// Row/Line height, in pixels

	// This assumes that FONT1 is monospaced!
	gr_get_string_size(&col_width, nullptr, " "); // Col/Character width, in pixels

	dc_scroll_x = 0;
	dc_scroll_y = 0;
	DCOLS = (gr_screen.center_w / col_width) - 1;	// Subtract as needed. Windowed mode has some quirks with the resolution
	DROWS = (gr_screen.center_h / row_height) - 2;
	DBCOLS = DCOLS;
	DBROWS = 2 * DROWS;

	// Init History
	dc_history.clear();
	dc_history.push_back("");
	last_oldcommand = dc_history.begin();

	// Init buffers
	dc_buffer.clear();
	dc_buffer.push_back("");
	
	dc_command_buf.reserve(MAX_CLI_LEN);
	dc_command_buf.clear();

	sprintf(dc_title, "FreeSpace Open v%s", FS_VERSION_FULL);
	dc_printf("Debug console started.\n" );
}

bool dc_pause_output(void)
{
	dc_printf("More to follow. Press any key to continue. ESC halts output...");

	int key;
	bool loop;
	do {
		loop = true;

		os_poll();

		dc_draw(FALSE);

		key = key_inkey();
		switch (key) {
		case KEY_ESC:
			return true;
			break;

		case KEY_PAGEUP:
			if (dc_scroll_y > 1) {
				dc_scroll_y--;
			}
			break;

		case KEY_PAGEDOWN:
			if (dc_scroll_y < DBROWS) {
				dc_scroll_y++;
			} else {
				dc_scroll_y = DBROWS;
			}
			break;

		case KEY_LEFT:
			// TODO: Scroll Left
			break;
		case KEY_RIGHT:
			// TODO: Scroll Right
			break;
		case 0:
			// No key pressed
			break;
		default:
			// Non-control key pressed, break.
			loop = false;
		}
	} while (loop);

	dc_printf("\n");
	return false;
};

void dc_printf(const char *format, ...)
{
	SCP_string tmp;
	va_list args;
	SCP_string::iterator tmp_it;

	va_start(args, format);
	vsprintf(tmp, format, args);
	va_end(args);

	for (tmp_it = tmp.begin(); tmp_it != tmp.end(); ++tmp_it) {
		dc_putc(*tmp_it);
	}
}

void dc_putc(char c)
{
	SCP_string* line_str = &(dc_buffer.back());
	SCP_string temp_str;
	int i;
	int w;

	if (c == ' ') {
		/**
		 * Push c onto the temp_str and get its gr_string width
		 *
		 * If we run out of room on the line, or 
		 * If we run out of room on the screen, change c to a '\n' and let subsequent block handle it,
		 * Else, push the space onto the line and bail
		 */
		temp_str = *line_str;
		temp_str.push_back(c);
		gr_get_string_size(&w, NULL, temp_str.c_str());

		if ((temp_str.size() >= DBCOLS) || (w > gr_screen.center_w)) {
			c = '\n';
		
		} else {
			lastwhite = temp_str.size();
			*line_str = temp_str;
			return;
		}
	}

	if (c == '\t') {
		/**
		 * Calculate how many spaces to put in to align tabs,
		 * Append temp_str with the spaces and get its gr_string width
		 *
		 * If we run out of room on the line, or
		 * If we run out of room on the screen, change c to a '\n' and let subsequent block handle it,
		 * Else, copy temp_str onto the line, update the lastwhite index, and bail
		 */
		i = DTABS - (line_str->size() % DTABS);
		temp_str = *line_str;
		temp_str.append(i, ' ');
		gr_get_string_size(&w, NULL, temp_str.c_str());

		if ((temp_str.size() >= DBCOLS) || (w > gr_screen.center_w)) {
			c = '\n';

		} else {
			lastwhite = temp_str.size();
			*line_str = temp_str;
			return;
		}
	}

	if (c == '\n') {
		/**
		 * Trash whatever char happens to be past (DBCOLS - 1),
		 * Push a blank line onto the dc_buffer from the bottom,
		 * Increment the scroller, if needed,
		 * Trash the topmost line(s) in the buffer,
		 * Reset the lastwhite index,
		 * Increment the lastline counter, and finally
		 * bail
		 */
		if (line_str->size() > DBCOLS) {
			line_str->resize(DBCOLS);
		}
		dc_buffer.push_back("");

		if ((dc_buffer.size() > DROWS) && (dc_scroll_y < SCROLL_Y_MAX)) {
			dc_scroll_y++;
		}

		while (dc_buffer.size() > DBROWS) {
			dc_buffer.pop_front();
		}

		lastwhite = 0;
		lastline++;
		return;
	}

	// By this point, c is probably a writable character
	temp_str = *line_str;
	temp_str.push_back(c);
	gr_get_string_size(&w, NULL, temp_str.c_str());

	if ((temp_str.size() >= DBCOLS) || (w > gr_screen.center_w)) {
		/**
		 * Word wrapping
		 * Save the word, clear the line of the word, push new line with the word on it
		 * Update scroll_y, if needed,
		 * Pop off old lines, and finally
		 * Push new character onto the new line
		 */
		temp_str = line_str->substr(lastwhite);
		line_str->resize(lastwhite);
		dc_buffer.push_back(std::move(temp_str));
		line_str = &dc_buffer.back();

		if ((dc_buffer.size() > DROWS) && (dc_scroll_y < SCROLL_Y_MAX)) {
			dc_scroll_y++;
		}

		while (dc_buffer.size() > DBROWS) {
			dc_buffer.pop_front();
		}

		lastwhite = 0;
		lastline++;
		line_str->push_back(c);
		return;
	}

	// Else, just push the char onto the line
	line_str->push_back(c);
}

bool handle_textInputEvent(const SDL_Event& event) {
	if (event.text.text[0] != '\0' && event.text.text[0] != '\1') {
		for (char c : event.text.text) {
			if (c < 32)
				break;

			if (dc_command_buf.size() < MAX_CLI_LEN) {
				dc_command_buf.push_back(c);
			}
		}
	}

	return true;
}

void debug_console(void (*_func)(void))
{
	int done = 0;

	while( key_inkey() ) {
		os_poll();
	}

	if ( !debug_inited ) {
		dc_init();
	}

	auto textListener = os::events::addEventListener(SDL_TEXTINPUT, 10000, &handle_textInputEvent);
	SDL_StartTextInput();

	dc_draw(TRUE);

	while (!done) {
		// poll the os
		os_poll();

		int k = key_inkey();
		switch( k ) {

		case KEY_SHIFTED+KEY_ENTER:
		case KEY_ESC:
			done = TRUE;
			break;

		case KEY_BACKSP:
			if (!dc_command_buf.empty()) {
				dc_command_buf.erase(dc_command_buf.size() - 1);
			}
			break;

		case KEY_F3:
		case KEY_UP:
			if (last_oldcommand < (dc_history.end() - 1)) {
				++last_oldcommand;
			}

			dc_command_buf = *last_oldcommand;
			break;

		case KEY_DOWN:
			if (last_oldcommand > dc_history.begin()) {
				--last_oldcommand;
			}

			dc_command_buf = *last_oldcommand;
			break;

		case KEY_PAGEUP:
			if (dc_scroll_y > 1) {
				dc_scroll_y--;
			}
			break;

		case KEY_PAGEDOWN:
			if (dc_scroll_y < (DBROWS - DROWS)) {
				dc_scroll_y++;
			} else {
				dc_scroll_y = (DBROWS - DROWS);
			}
			break;

		case KEY_ENTER:
			dc_scroll_y = (DBROWS - DROWS);			// Set the scroll to look at the bottom
			last_oldcommand = dc_history.begin();	// Reset the last oldcommand
			lastline = 0;	// Reset the line counter

			// Clear the command line on the window, but don't print the prompt until the command has processed
			// Stuff a copy of the command line onto the history
			// Search for the command
				// If not found:
				//   abort,
				//   dc_printf("Error: Invalid or Missing command %s", cmd.c_str()), and
				//   dc_printf(dc_prompt) when ready for input
			// Call the function for that command, and strip the cmd token from the command line string
			if (dc_command_buf.empty()) {
				dc_printf("No command given.\n");
				break;
			} // Else, continue to process the cmd_line

			// z64: Thread Note: Maybe lock a mutex here to allow a previous DCF to finish/abort before starting a new one
			// z64: We'll just assume we won't be here unless a command has finished...
			dc_history.push_front(dc_command_buf);	// Push the command onto the history queue
			last_oldcommand = dc_history.begin();	// Reset oldcommand

			while (dc_history.size() > DCMDS) {
				dc_history.pop_back();			// Keep the commands less than or equal to DCMDS
			}

			dc_command_str = dc_command_buf;	// Xfer to the command string for processing
			dc_command_buf.resize(0);			// Nullify the buffer
			dc_printf("%s%s\n", dc_prompt, dc_command_str.c_str());	// Print the command w/ prompt.
			dc_draw(FALSE);					// Redraw the console without the command line.
			dc_do_command(&dc_command_str);	// Try to do the command
			break;

		default:
			// Leave handling of key input to SDL
			break;

		}

		// Do the passed function
		if ( _func ) {
			_func();
		}

		// All done, and ready for new entry
		dc_draw(TRUE);
	}

	SDL_StopTextInput();
	os::events::removeEventListener(textListener);

	while( key_inkey() ) {
		os_poll();
	}
}
