/******************************************************************************
	Project: 	MicroMacro
	Author: 	SolarStrike Software
	URL:		www.solarstrike.net
	License:	Modified BSD (see license.txt)
******************************************************************************/

#include "ncurses_lua.h"
#include "event.h"
#include "macro.h"
#include "strl.h"
#include "luatypes.h"

#include "global_addon.h"

#include <algorithm>

extern "C"
{
	#include <lua.h>
	#include <lauxlib.h>
	#include <lualib.h>
}

bool Ncurses_lua::initialized = false;
const char *Ncurses_lua::stdscr_name = "STDSCR";
unsigned int Ncurses_lua::historyIndex = 0;
std::vector<std::string> Ncurses_lua::history;

int Ncurses_lua::is_initialized()
{
	return initialized;
}

int Ncurses_lua::regmod(lua_State *L)
{
	static const luaL_Reg _funcs[] = {
		{"init", Ncurses_lua::init},
		{"cleanup", Ncurses_lua::cleanup},
		{"print", Ncurses_lua::print},
		{"refresh", Ncurses_lua::refresh},
		{"scrollok", Ncurses_lua::scrollok},
		{"clear", Ncurses_lua::clear},
		{"move", Ncurses_lua::move},
		{"createWindow", Ncurses_lua::createWindow},
		{"resizeWindow", Ncurses_lua::resizeWindow},
		{"moveWindow", Ncurses_lua::moveWindow},
		{"getString", Ncurses_lua::getString},
		{"setPair", Ncurses_lua::setPair},
		{"getPair", Ncurses_lua::getPair},
		{"attributeOn", Ncurses_lua::attributeOn},
		{"attributeOff", Ncurses_lua::attributeOff},
		{"setAttribute", Ncurses_lua::setAttribute},
		{"getAttribute", Ncurses_lua::getAttribute},
		{"setBackground", Ncurses_lua::setBackground},
		{"getWindowSize", Ncurses_lua::getWindowSize},
		{NULL, NULL}
	};

	luaL_newlib(L, _funcs);

	/* Set global vars */
	// Colors
	lua_pushinteger(L, COLOR_BLACK);
	lua_setfield(L, -2, "BLACK");
	lua_pushinteger(L, COLOR_RED);
	lua_setfield(L, -2, "RED");
	lua_pushinteger(L, COLOR_GREEN);
	lua_setfield(L, -2, "GREEN");
	lua_pushinteger(L, COLOR_YELLOW);
	lua_setfield(L, -2, "YELLOW");
	lua_pushinteger(L, COLOR_BLUE);
	lua_setfield(L, -2, "BLUE");
	lua_pushinteger(L, COLOR_MAGENTA);
	lua_setfield(L, -2, "MAGENTA");
	lua_pushinteger(L, COLOR_CYAN);
	lua_setfield(L, -2, "CYAN");
	lua_pushinteger(L, COLOR_WHITE);
	lua_setfield(L, -2, "WHITE");

	// Styles
	lua_pushinteger(L, A_NORMAL);
	lua_setfield(L, -2, "NORMAL");
	lua_pushinteger(L, A_STANDOUT);
	lua_setfield(L, -2, "STANDOUT");
	lua_pushinteger(L, A_UNDERLINE);
	lua_setfield(L, -2, "UNDERLINE");
	lua_pushinteger(L, A_REVERSE);
	lua_setfield(L, -2, "REVERSE");
	lua_pushinteger(L, A_BLINK);
	lua_setfield(L, -2, "BLINK");
	lua_pushinteger(L, A_DIM);
	lua_setfield(L, -2, "DIM");
	lua_pushinteger(L, A_BOLD);
	lua_setfield(L, -2, "BOLD");
	lua_pushinteger(L, A_PROTECT);
	lua_setfield(L, -2, "PROTECT");
	lua_pushinteger(L, A_INVIS);
	lua_setfield(L, -2, "INVIS");
	lua_pushinteger(L, A_ALTCHARSET);
	lua_setfield(L, -2, "ALTCHARSET");
	lua_pushinteger(L, A_CHARTEXT);
	lua_setfield(L, -2, "CHARTEXT");

	lua_setglobal(L, NCURSES_MODULE_NAME);

	return MicroMacro::ERR_OK;
}

int Ncurses_lua::init(lua_State *L)
{
	::initscr();
	::cbreak();
	::noecho();
	::keypad(::stdscr, true);
	::leaveok(::stdscr, true);
	::start_color();

	historyIndex = 0;

	/*	Certain mouse events cause blocking issues in non-blocking mode.
		To avoid this, lets only listen to a single event that doesn't
		cause any issues. Note that using 0 for the mask also doesn't help.
	*/
	mousemask(BUTTON1_PRESSED, NULL);

	// Move/hide cursor
	int y,x;
	getmaxyx(::stdscr, y, x);
	mvcur(0, 0, y-1, x-1);
	curs_set(0);
	//hideCursor();

	// Forecefully clear the screen, reset style
	wbkgd(::stdscr, A_NORMAL);
	wattrset(::stdscr, A_NORMAL);
	wclear(::stdscr);
	wrefresh(::stdscr);

	// Create a new window, put it on the Lua stack
	WINDOW **w = (WINDOW **)lua_newuserdata(L, sizeof(WINDOW *));
	*w = ::stdscr;

	// Set metatable
	luaL_getmetatable(L, LuaType::metatable_ncursesWindow);
	lua_setmetatable(L, -2);

	// Set Lua var (global namespace)
	lua_setglobal(L, Ncurses_lua::stdscr_name);

	initialized = true;
	return 0;
}

int Ncurses_lua::cleanup(lua_State *L)
{
	// Clear history
	history.clear();
	historyIndex = 0;

	// Reset text/BG style
	wbkgd(::stdscr, A_NORMAL);
	wattrset(::stdscr, A_NORMAL);

	// Clear stdscr
	werase(::stdscr);
	wrefresh(::stdscr);

	// Close ncurses
	endwin();

	// Unset vars
	lua_getglobal(L, NCURSES_MODULE_NAME);
	lua_pushnil(L);
	lua_setfield(L, -2, Ncurses_lua::stdscr_name);
	lua_pop(L, 1);

	initialized = false;
	return 0;
}

/*	ncurses.print(window win, string str)
	Returns:	nil

	Prints 'str' onto window 'win' at current cursor position.
	Remember to refresh the screen afterwards!
*/
int Ncurses_lua::print(lua_State *L)
{
	int top = lua_gettop(L);
	if( top < 2 )
		wrongArgs(L);
	checkType(L, LT_USERDATA, 1);
	checkType(L, LT_STRING, 2);

	WINDOW **pw = NULL;
	pw = (WINDOW **)lua_touserdata(L, 1);

	if( top == 2 )
		wprintw(*pw, lua_tostring(L, 2));
	else
	{
		lua_remove(L, 1); // Toss out the Window pointer
		Global_addon::sprintf(L); // Format it, put result on top of stack
		if( lua_isstring(L, -1) ) // If we can, print it
			wprintw(*pw, lua_tostring(L, -1));
	}

	return 0;
}

/*	ncurses.refresh(window win)
	Returns:	nil

	Redraw the given window to commit our changes.
*/
int Ncurses_lua::refresh(lua_State *L)
{
	if( lua_gettop(L) != 1 )
		wrongArgs(L);
	checkType(L, LT_USERDATA, 1);

	WINDOW **pw = NULL;
	pw = (WINDOW **)lua_touserdata(L, 1);

	wrefresh(*pw);
	return 0;
}

/*	ncurses.scrollok(window win, boolean scrolling)
	Returns:	nil

	Sets (or unsets) scrolling on the given window.
*/
int Ncurses_lua::scrollok(lua_State *L)
{
	if( lua_gettop(L) != 2 )
		wrongArgs(L);
	checkType(L, LT_USERDATA, 1);
	checkType(L, LT_BOOLEAN, 2);

	WINDOW **pw = NULL;
	pw = (WINDOW **)lua_touserdata(L, 1);

	::scrollok(*pw, (bool)lua_toboolean(L, 2));
	return 0;
}

/*	ncurses.clear(window win)
	Returns:	nil

	Erase the contents of window 'win'.
*/
int Ncurses_lua::clear(lua_State *L)
{
	if( lua_gettop(L) != 1 )
		wrongArgs(L);
	checkType(L, LT_USERDATA, 1);

	WINDOW **pw = NULL;
	pw = (WINDOW **)lua_touserdata(L, 1);

	werase(*pw);
	return 0;
}

/*	ncurses.move(window win, number y, number x)
	Returns:	nil

	Moves the position of the cursor to y,x in the given window.
*/
int Ncurses_lua::move(lua_State *L)
{
	if( lua_gettop(L) != 3 )
		wrongArgs(L);
	checkType(L, LT_USERDATA, 1);
	checkType(L, LT_NUMBER, 2);
	checkType(L, LT_NUMBER, 3);

	WINDOW **pw = NULL;
	pw = (WINDOW **)lua_touserdata(L, 1);

	int y = lua_tointeger(L, 2);
	int x = lua_tointeger(L, 3);
	wmove(*pw, y, x);
	wrefresh(*pw);

	return 0;
}

/*	ncurses.createWindow(number sy, number sx, number width, number height)
	Returns (on success):	userdata (window)
	Returns (on failure):	nil

	Creates a new Ncurses window and returns it.
	'sy' and 'sx' indicate the top-left corner of the window (in characters).
	'width' and 'height' represent the window's width/height in characters.
*/
int Ncurses_lua::createWindow(lua_State *L)
{
	if( lua_gettop(L) != 4 )
		wrongArgs(L);
	checkType(L, LT_NUMBER, 1);
	checkType(L, LT_NUMBER, 2);
	checkType(L, LT_NUMBER, 3);
	checkType(L, LT_NUMBER, 4);

	int height, width, starty, startx;
	WINDOW **pw;
	starty = lua_tointeger(L, 1);
	startx = lua_tointeger(L, 2);
	height = lua_tointeger(L, 3);
	width = lua_tointeger(L, 4);

	WINDOW *nWin = newwin(height, width, starty, startx);
	if( nWin == NULL )
	{ // Throw an error
		pushLuaErrorEvent(L, "Error creating Ncurses window.");
		return 0;
	}

	// If it worked, we can create a userdata and set it up
	pw = (WINDOW **)lua_newuserdata(L, sizeof(WINDOW *));
	*pw = nWin;
	leaveok(*pw, true);
	keypad(*pw, true);

	// Set metatable
	luaL_getmetatable(L, LuaType::metatable_ncursesWindow);
	lua_setmetatable(L, -2);

	return 1;
}

/*	ncurses.resizeWindow(window win, number lines, number columns)
	Returns:	boolean

	Resize a window to the given columns/lines.
	Returns true on success, false on failure.
*/
int Ncurses_lua::resizeWindow(lua_State *L)
{
	if( lua_gettop(L) != 3 )
		wrongArgs(L);
	checkType(L, LT_USERDATA, 1);
	checkType(L, LT_NUMBER, 2);
	checkType(L, LT_NUMBER, 3);

	WINDOW **pw = NULL;
	pw = (WINDOW **)lua_touserdata(L, 1);

	int lines = lua_tointeger(L, 2);
	int columns = lua_tointeger(L, 3);
	int success = wresize(*pw, lines, columns);

	lua_pushboolean(L, success != ERR);
	return 1;
}

/*	ncurses.moveWindow(window win, number y, number x)
	Returns:	boolean

	Move a window to the given position.
	Returns true on success, false on failure.
*/
int Ncurses_lua::moveWindow(lua_State *L)
{
	if( lua_gettop(L) != 3 )
		wrongArgs(L);
	checkType(L, LT_USERDATA, 1);
	checkType(L, LT_NUMBER, 2);
	checkType(L, LT_NUMBER, 3);

	WINDOW **pw = NULL;
	pw = (WINDOW **)lua_touserdata(L, 1);

	int y = lua_tointeger(L, 2);
	int x = lua_tointeger(L, 3);
	int success = mvwin(*pw, y, x);

	lua_pushboolean(L, success != ERR);
	return 1;
}


/* NOTE: This seems broken in this version of Ncurses... disabled for now
void Ncurses_lua::hideCursor()
{
	CONSOLE_CURSOR_INFO cci;
	cci.dwSize = 50;
	cci.bVisible = false;

	int s = SetConsoleCursorInfo(Macro::instance()->getAppHandle(), &cci);
	curs_set(0);
}

void Ncurses_lua::showCursor()
{
	CONSOLE_CURSOR_INFO cci;
	cci.dwSize = 1;
	cci.bVisible = true;

	SetConsoleCursorInfo(Macro::instance()->getAppHandle(), &cci);
	curs_set(1);
}
*/

/*	NOTE: Not exposed to Lua
	Discards buffered keyboard input for given window
*/
void Ncurses_lua::flush(WINDOW *pw)
{
	FlushConsoleInputBuffer(Macro::instance()->getAppHandle()); // Flush Windows' input
	::flushinp(); // Flush Ncurses input

	/*	Because for some reason there's still crap in the buffers...
		We manually read it till it's empty.
		Set to nodelay so we can catch its emptiness instead of freeze.
	*/
	nodelay(pw, true);
	int c = wgetch(pw);
	while( c != ERR )
	{
		if( c == KEY_MOUSE )
		{
			MEVENT e;
			getmouse(&e);
		}
		c = wgetch(pw);
	}

	nodelay(pw, false);
}

/* NOTE: Not exposed to Lua
	This is essentially a non-broken getline() for a given
	Ncurses window.
*/
void Ncurses_lua::readline(WINDOW *pw, char *buffer, size_t bufflen)
{
	historyIndex = history.size();

	unsigned int pos = 0;
	unsigned int len = 0;
	unsigned int outstart = 0;
	unsigned int outwidth = 0;
	int x, y, mx, my;

	memset(buffer, 0, bufflen);
	getyx(pw, y, x);
	getmaxyx(pw, my, mx);
	outwidth = mx - x - 1;

	bool scrolling = pw->_scroll;

	if( scrolling ) // Temp disable
		::scrollok(pw, false);

	while(true)
	{
		int c; // Input character

		if( pos < outstart )
			outstart = (pos >= 5 ? pos - 5 : 0); // Just a nice touch when we're moving left
		else if( pos > (outstart + outwidth) )
			outstart = pos - outwidth;
		if( outstart > len )
			outstart = len;

		for(unsigned int i = 0; i <= outwidth; i++)
			mvwaddch(pw, y, x+i, ' '); // Erase garbage
		mvwaddnstr(pw, y, x, buffer + outstart, (len < outwidth ? len : outwidth)); // len-outstart ?
		wmove(pw, y, x + (pos - outstart));

		wrefresh(pw);
		c = wgetch(pw);

		if( c == KEY_ENTER || c == '\n' || c == '\r' )
			break;
		else if( c == KEY_LEFT )
			if( pos > 0 ) --pos; else beep();
		else if( c == KEY_RIGHT )
			if( pos < len ) ++pos; else beep();
		else if( c == KEY_UP && historyIndex > 0 )
		{
			--historyIndex;
			std::string str = history.at(historyIndex).c_str();
			strlcpy(buffer, str.c_str(), bufflen);
			len = str.length();
			pos = len;
		}
		else if( c == KEY_DOWN && historyIndex < (history.size() - 1)  )
		{
			++historyIndex;
			std::string str = history.at(historyIndex).c_str();
			strlcpy(buffer, str.c_str(), bufflen);
			len = str.length();
			pos = len;
		}
		else if( c == KEY_HOME )
			pos = 0;
		else if( c == KEY_END )
			pos = len;
		else if( c == VK_BACK || c == KEY_BACKSPACE ) // Remove left char
			if( pos > 0 ) {
				memmove(buffer+pos-1, buffer+pos, len-pos);
				buffer[len] = '\0';
				--pos; --len;
			}
			else beep();
		else if( c == KEY_DC ) // Remove pos char
			if( pos <= len && len > 0 ) {
				if( pos < len )
				{
					memmove(buffer+pos, buffer+pos+1, len-pos);
					buffer[len] = '\0';
					--len;
				}
				if( pos > len )
					pos = len;
			} else beep();
		else if( c >= ' ' && c <= '~' ) // Make sure it is even printable
		{
			if( pos + 2 < bufflen ) // Make sure we're not overflowing
			{
				if( pos == len ) // Append character
				{
					buffer[pos] = c;
					buffer[len+1] = 0; // NULL-terminator
					++pos; ++len;
				}
				else // Insert character
				{
					memmove(buffer+pos+1, buffer+pos, len-pos); // Shift characters right
					buffer[pos] = c; // Insert new character
					++pos; ++len;
				}
			}
			else beep();
		}
	}

	// Re-print it, in case the user repositioned the pointer, Ncurses might clear to EOL
	mvwaddnstr(pw, y, x, buffer + outstart, (len < outwidth ? len : outwidth));
	wrefresh(pw);

	if( scrolling ) // Re-enable
		::scrollok(pw, true);

	buffer[len] = 0; // Ensure NULL terminator

	// Remove dropped history
	if( history.size() >= MAX_NCURSES_HISTORY_LINES ) // Don't keep too many
		history.erase(history.begin(), history.begin()+1);

	// Insert history entry
	history.push_back(buffer);
}

/*	ncurses.getString(window win)
	Returns:	string

	Prompt the user for some input on an Ncurses window.
	Returns that input as a string value.

	NOTE: This function is blocking!
*/
int Ncurses_lua::getString(lua_State *L)
{
	if( lua_gettop(L) != 1 )
		wrongArgs(L);
	checkType(L, LT_USERDATA, 1);

	WINDOW **pw = NULL;
	pw = (WINDOW **)lua_touserdata(L, 1);

	char buffer[1024];
	int sx, sy, nx, ny;

	// Flush input, prep window for input
	flush(*pw);

	leaveok(*pw, false);
	getbegyx(*pw, sy, sx);
	getyx(*pw, ny, nx);
	mvcur(0, 0, sy+ny, sx+nx);

	// Do input
	readline(*pw, buffer, sizeof(buffer)-1);

	// Output a newline, but we don't need to refresh
	wprintw(*pw, "\n");

	// Change back for output mode again
	int y,x;
	getmaxyx(::stdscr, y, x);
	mvcur(0, 0, y-1, x-1);
	leaveok(*pw, true);

	lua_pushstring(L, buffer);
	return 1;
}

/* NOTE: Not supported on Windows console
int Ncurses_lua::setColor(lua_State *L)
{
	if( lua_gettop(L) != 4 )
		wrongArgs(L);
	checkType(L, LT_NUMBER, 1);
	checkType(L, LT_NUMBER, 2);
	checkType(L, LT_NUMBER, 3);
	checkType(L, LT_NUMBER, 4);

	short colIndex = (short)lua_tonumber(L, 1);
	int colR = lua_tointeger(L, 2);
	int colG = lua_tointeger(L, 3);
	int colB = lua_tointeger(L, 4);

	return 0;
}
*/

/*	ncurses.setPair(number pairIndex, number foregroundColor, number backgroundColor)
	Returns:	nil

	Modifies the color (foreground/background) pair at a given index.
*/
int Ncurses_lua::setPair(lua_State *L)
{
	if( lua_gettop(L) != 3 )
		wrongArgs(L);
	checkType(L, LT_NUMBER, 1);
	checkType(L, LT_NUMBER, 2);
	checkType(L, LT_NUMBER, 3);

	int pairIndex = lua_tointeger(L, 1);
	int foreground = lua_tointeger(L, 2);
	int background = lua_tointeger(L, 3);

	// Ensure index is not out of bounds
	if( pairIndex < 1 || pairIndex >= COLOR_PAIRS )
		return 0;

	// Constrain colors
	foreground = std::max(0, foreground);
	foreground = std::min(foreground, COLORS);
	background = std::max(0, background);
	background = std::min(background, COLORS);

	// Do it!
	init_pair(pairIndex, foreground, background);

	return 0;
}

/*	ncurses.getPair(number pairIndex)
	Returns:	number (foreground/background pair)

	Returns the attribute mask of a given color pair.
*/
int Ncurses_lua::getPair(lua_State *L)
{
	if( lua_gettop(L) != 1 )
		wrongArgs(L);
	checkType(L, LT_NUMBER, 1);

	int pairIndex = lua_tointeger(L, 1);

	// Ensure index is not out of bounds
	if( pairIndex < 0 || pairIndex >= COLOR_PAIRS )
		return 0;

	lua_pushnumber(L, COLOR_PAIR(pairIndex));
	return 1;
}

/*	ncurses.attributeOn(window win, number attrib)
	Returns:	nil

	Turns an attribute (identified by 'attrib') on for the given window.
*/
int Ncurses_lua::attributeOn(lua_State *L)
{
	if( lua_gettop(L) != 2 )
		wrongArgs(L);
	checkType(L, LT_USERDATA, 1);
	checkType(L, LT_NUMBER, 2);

	WINDOW **pw = NULL;
	pw = (WINDOW **)lua_touserdata(L, 1);
	int attribValue = lua_tointeger(L, 2);
	wattron(*pw, attribValue);

	return 0;
}


/*	ncurses.attributeOff(window win, number attrib)
	Returns:	nil

	Turns an attribute (identified by 'attrib') off for the given window.
*/
int Ncurses_lua::attributeOff(lua_State *L)
{
	if( lua_gettop(L) != 2 )
		wrongArgs(L);
	checkType(L, LT_USERDATA, 1);
	checkType(L, LT_NUMBER, 2);

	WINDOW **pw = NULL;
	pw = (WINDOW **)lua_touserdata(L, 1);
	int attribValue = lua_tointeger(L, 2);
	wattroff(*pw, attribValue);

	return 0;
}


/*	ncurses.setAttribute(window win, number attrib)
	Returns:	nil

	Sets an attribute (identified by 'attrib') on the given window.
*/
int Ncurses_lua::setAttribute(lua_State *L)
{
	if( lua_gettop(L) != 2 )
		wrongArgs(L);
	checkType(L, LT_USERDATA, 1);
	checkType(L, LT_NUMBER, 2);

	WINDOW **pw = NULL;
	pw = (WINDOW **)lua_touserdata(L, 1);
	int attribValue = lua_tointeger(L, 2);
	wattrset(*pw, attribValue);

	return 0;
}

/*	ncurses.getAttribute(window win)
	Returns (on success):	number attributes
							number pair

	Returns (on failure):	nil

	Returns the attribute mask and color pair for a Ncurses window.
*/
int Ncurses_lua::getAttribute(lua_State *L)
{
	if( lua_gettop(L) != 1 )
		wrongArgs(L);
	checkType(L, LT_USERDATA, 1);

	WINDOW **pw = NULL;
	pw = (WINDOW **)lua_touserdata(L, 1);

	attr_t attribs;
	short pair;
	int success = wattr_get(*pw, &attribs, &pair, NULL);

	if( success == ERR )
		return 0;

	lua_pushinteger(L, attribs); // An attr_t is basically just an unsigned int
	lua_pushinteger(L, pair); // Pair is a short, but same deal
	return 2;
}

/*	ncurses.setBackground(window win, number attrib)
	Returns:	nil

	Sets a background on the given window to the attribute mask.
*/
int Ncurses_lua::setBackground(lua_State *L)
{
	if( lua_gettop(L) != 2 )
		wrongArgs(L);
	checkType(L, LT_USERDATA, 1);
	checkType(L, LT_NUMBER, 2);

	WINDOW **pw = NULL;
	pw = (WINDOW **)lua_touserdata(L, 1);

	int style = lua_tointeger(L, 2);
	attr_t attribs = style;
	wbkgd(*pw, attribs);
	return 0;
}


/*	ncurses.getWindowSize(window win)
	Returns:	number y
				number x

	Returns the size of a window in characters.
*/
int Ncurses_lua::getWindowSize(lua_State *L)
{
	if( lua_gettop(L) != 1 )
		wrongArgs(L);
	checkType(L, LT_USERDATA, 1);

	WINDOW **pw = NULL;
	pw = (WINDOW **)lua_touserdata(L, 1);

	int y, x;
	getmaxyx(*pw, y, x);

	lua_pushnumber(L, y);
	lua_pushnumber(L, x);

	return 2;
}

void Ncurses_lua::safeDestroy(WINDOW *pWin)
{
	if( pWin != ::stdscr )
		delwin(pWin);
}
