// mConsole.cpp
// ````````````

// TODO: arrows for editing, history, wchar* versions(?)

#include <string>
#include <conio.h>   // _kbhit, _getch
#include <cstdio>    // puts, printf
#include <cstdarg>   // v*printf
#include <Windows.h> // Sleep, Console functions

#include "mConsole.h"

#define LINEWIDTH 80
#define TRUNC_TEXT "(...)"
#define TRUNC_TEXT_LEN 5

struct mConsoleData {
	std::string buff;
	std::string prompt;
	HANDLE hStdout;
	char blankLine[LINEWIDTH]; // n.b. Not null-terminated
};

// Erases the current line and returns the cursor
void mConsole::_eraseCurrentLine() {
	CONSOLE_SCREEN_BUFFER_INFO csbi = {0};
	COORD new_cursor_pos = {0};
				
	// Move cursor to start of current line
	GetConsoleScreenBufferInfo(_d->hStdout, &csbi);
	new_cursor_pos.Y = csbi.dwCursorPosition.Y;
	SetConsoleCursorPosition(_d->hStdout, new_cursor_pos);
		
	// Write a blank line
	WriteConsoleA(_d->hStdout, _d->blankLine, LINEWIDTH - 1, 0, 0);
		
	// Return cursor to start of line
	new_cursor_pos.Y = csbi.dwCursorPosition.Y;
	SetConsoleCursorPosition(_d->hStdout, new_cursor_pos);
}
	
// Replace the input buffer
void mConsole::_redrawInputBuffer() {
	_eraseCurrentLine();
	_displayInputBuffer();
}
	
// Displays the input buffer
void mConsole::_displayInputBuffer() {
	unsigned int maxlen = LINEWIDTH; // - 1;
		
	// Determine how to truncate the display
	std::string display;
	if (_d->prompt.length() + _d->buff.length() > maxlen) {
		int numc = maxlen - _d->prompt.length() - TRUNC_TEXT_LEN;
		if (numc < 0) {
			// Can't fit anything. Truncate the prompt and input
			display = _d->prompt + _d->buff;
			display =
				TRUNC_TEXT
				+ display.substr(display.length() - LINEWIDTH + TRUNC_TEXT_LEN);
		} else {
			// Can fit the whole prompt, so only truncate the input
			display =
				_d->prompt
				+ TRUNC_TEXT
				+ _d->buff.substr(_d->buff.length() - numc);
		}
	} else {
		// Everything fits on screen
		display = _d->prompt + _d->buff;
	}
		
	if (display.length() > LINEWIDTH-1) {
		::fwrite(display.c_str(), sizeof(char), LINEWIDTH-1, stdout);
	} else {
		::fputs(display.c_str(), stdout);
	}
}
	

// Constructor
mConsole::mConsole() {
	// Initialise private members
	_d = new mConsoleData;
	_d->buff.assign("");
	_d->prompt.assign("");
	for (int i=0; i < LINEWIDTH; i++) _d->blankLine[i] = ' ';
						
	// Cache STD handles
	_d->hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
}
	
// Deconstructor
mConsole::~mConsole() {
	// Free private members
	delete _d;
}
	
// Print string without interfering with the input line
void mConsole::puts(const char* text) {
	_eraseCurrentLine();
	::puts(text);
	_displayInputBuffer();
}

// Print string without interfering with the input line
void mConsole::puts_nonewline(const char* text) {
	_eraseCurrentLine();
	::fputs(text, stdout);
	_displayInputBuffer();
}
	
// Print formatted string without interfering with the input line
void mConsole::printf(const char* fmt, ...) {	
	// Prepare vargs
	if (! fmt) return;	
	va_list args;
	va_start(args, fmt);
		
	// sprintf into a buffer
	int required_length = vsnprintf(0, 0, fmt, args);
	char* sbuffer = new char[required_length + 1];
	vsnprintf(sbuffer, required_length, fmt, args);
	sbuffer[required_length] = 0;
	
	// Display and tidy up
	this->puts_nonewline(sbuffer);
	delete[] sbuffer;
		
	va_end(args);
}
	
// Manage input events
bool mConsole::manageInput() {
	if (! _kbhit()) return false;
	int c = _getch();
		
	// Manage keypress
	switch (c) {
		case 0: {   // Control code (F-keys)
			// Ignore next;
			_getch();
		} break;
		case 8: {   // Backspace
			_d->buff = _d->buff.substr(0, _d->buff.length() - 1);
			_redrawInputBuffer();
		} break;
		case 13: {  // Enter, push command
			return true;
		} break;
		case 27: {  // Escape, ignore
		} break;
		case 224: { // Control code (arrows)
			// Ignore next;
			_getch();
		} break;
		default: {			
			char cc[2] = {c, 0};
			_d->buff.append(cc);
			_redrawInputBuffer();
		} break;
	}
	return false;
}
	
// Set input buffer
void mConsole::setLine(const char* inBuff) {
	_d->buff.assign(inBuff);
	_redrawInputBuffer();
}

// Retrieve buffer
void mConsole::getLine(std::string& outBuff) {
	outBuff.assign(_d->buff);
	_d->buff.assign("");
	_redrawInputBuffer();
}

// Assigns the prompt
void mConsole::setPrompt(const char* prompt_str) {
	_d->prompt.assign(prompt_str);
	_redrawInputBuffer();
}

// Retrieve the prompt
void mConsole::getPrompt(std::string& outBuff) {
	outBuff.assign(_d->prompt);
}