/*
===========================================================================
Copyright (C) 1999-2005 Id Software, Inc.

This file is part of Quake III Arena source code.

Quake III Arena source code 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; either version 2 of the License,
or (at your option) any later version.

Quake III Arena source code 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.

You should have received a copy of the GNU General Public License
along with Quake III Arena source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/

#include "../qcommon/q_shared.h"
#include "../qcommon/qcommon.h"
#include "sys_local.h"

#ifndef DEDICATED
#include "../client/client.h"
#endif

#include <unistd.h>
#include <signal.h>
#include <termios.h>
#include <fcntl.h>
#include <sys/time.h>

/*
=============================================================
tty console routines

NOTE: if the user is editing a line when something gets printed to the early
console then it won't look good so we provide CON_Hide and CON_Show to be
called before and after a stdout or stderr output
=============================================================
*/

extern qboolean ConsoleIsPassive;

extern qboolean stdinIsATTY;
static qboolean stdin_active;
// general flag to tell about tty console mode
static qboolean ttycon_on = qfalse;
static int ttycon_hide = 0;
static int ttycon_show_overdue = 0;

// some key codes that the terminal may be using, initialised on start up
static int TTY_erase;
static int TTY_eof;

static struct termios TTY_tc;

static field_t TTY_con;

// This is somewhat of aduplicate of the graphical console history
// but it's safer more modular to have our own here
#define CON_HISTORY 32
static field_t ttyEditLines[ CON_HISTORY ];
static int hist_current = -1, hist_count = 0;

#ifndef DEDICATED
// Don't use "]" as it would be the same as in-game console,
//   this makes it clear where input came from.
#define TTY_CONSOLE_PROMPT "tty]"
#else
#define TTY_CONSOLE_PROMPT "]"
#endif

/*
==================
CON_Back

Output a backspace

NOTE: it seems on some terminals just sending '\b' is not enough so instead we
send "\b \b"
(FIXME there may be a way to find out if '\b' alone would work though)
==================
*/
static void CON_Back( void )
{
	char key;
	size_t UNUSED_VAR size;

	key = '\b';
	size = write(STDOUT_FILENO, &key, 1);
	key = ' ';
	size = write(STDOUT_FILENO, &key, 1);
	key = '\b';
	size = write(STDOUT_FILENO, &key, 1);
}

/*
==================
CON_Hide

Clear the display of the line currently edited
bring cursor back to beginning of line
==================
*/
static void CON_Hide( void )
{
	if( ttycon_on )
	{
		int i;
		if (ttycon_hide)
		{
			ttycon_hide++;
			return;
		}
		if (TTY_con.cursor>0)
		{
			for (i=0; i<TTY_con.cursor; i++)
			{
				CON_Back();
			}
		}
		// Delete prompt
		for (i = strlen(TTY_CONSOLE_PROMPT); i > 0; i--) {
			CON_Back();
		}
		ttycon_hide++;
	}
}

/*
==================
CON_Show

Show the current line
FIXME need to position the cursor if needed?
==================
*/
static void CON_Show( void )
{
	if( ttycon_on )
	{
		int i;

		assert(ttycon_hide>0);
		ttycon_hide--;
		if (ttycon_hide == 0)
		{
			size_t UNUSED_VAR size;
			size = write(STDOUT_FILENO, TTY_CONSOLE_PROMPT, strlen(TTY_CONSOLE_PROMPT));
			if (TTY_con.cursor)
			{
				for (i=0; i<TTY_con.cursor; i++)
				{
					int n;
					//size = write(STDOUT_FILENO, TTY_con.buffer+i, 1);
					for (n = 0;  n < TTY_con.xbuffer[i].numUtf8Bytes;  n++) {
						size = write(STDOUT_FILENO, &TTY_con.xbuffer[i].utf8Bytes[n], 1);
					}
				}
			}
		}
	}
}

/*
==================
CON_Shutdown

Never exit without calling this, or your terminal will be left in a pretty bad state
==================
*/
void CON_Shutdown( void )
{
	if (ConsoleIsPassive) {
		return;
	}

	if (ttycon_on)
	{
		CON_Hide();
		tcsetattr (STDIN_FILENO, TCSADRAIN, &TTY_tc);
	}

	// Restore blocking to stdin reads
	fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL, 0) & ~O_NONBLOCK);
}

/*
==================
Hist_Add
==================
*/
void Hist_Add(field_t *field)
{
	int i;

	// Don't save blank lines in history.
	if (!field->cursor)
		return;

	assert(hist_count <= CON_HISTORY);
	assert(hist_count >= 0);
	assert(hist_current >= -1);
	assert(hist_current <= hist_count);
	// make some room
	for (i=CON_HISTORY-1; i>0; i--)
	{
		ttyEditLines[i] = ttyEditLines[i-1];
	}
	ttyEditLines[0] = *field;
	if (hist_count<CON_HISTORY)
	{
		hist_count++;
	}
	hist_current = -1; // re-init
}

/*
==================
Hist_Prev
==================
*/
field_t *Hist_Prev( void )
{
	int hist_prev;
	assert(hist_count <= CON_HISTORY);
	assert(hist_count >= 0);
	assert(hist_current >= -1);
	assert(hist_current <= hist_count);
	hist_prev = hist_current + 1;
	if (hist_prev >= hist_count)
	{
		return NULL;
	}
	hist_current++;
	return &(ttyEditLines[hist_current]);
}

/*
==================
Hist_Next
==================
*/
field_t *Hist_Next( void )
{
	assert(hist_count <= CON_HISTORY);
	assert(hist_count >= 0);
	assert(hist_current >= -1);
	assert(hist_current <= hist_count);
	if (hist_current >= 0)
	{
		hist_current--;
	}
	if (hist_current == -1)
	{
		return NULL;
	}
	return &(ttyEditLines[hist_current]);
}

/*
==================
CON_SigCont
Reinitialize console input after receiving SIGCONT, as on Linux the terminal seems to lose all
set attributes if user did CTRL+Z and then does fg again.
==================
*/

void CON_SigCont(int signum)
{
	CON_Init();
}

/*
==================
CON_Init

Initialize the console input (tty mode if possible)
==================
*/
void CON_Init( void )
{
	struct termios tc;

	if (ConsoleIsPassive) {
		return;
	}

	// If the process is backgrounded (running non interactively)
	// then SIGTTIN or SIGTOU is emitted, if not caught, turns into a SIGSTP
	signal(SIGTTIN, SIG_IGN);
	signal(SIGTTOU, SIG_IGN);

	// If SIGCONT is received, reinitialize console
	signal(SIGCONT, CON_SigCont);

	// Make stdin reads non-blocking
	fcntl(STDIN_FILENO, F_SETFL, fcntl(STDIN_FILENO, F_GETFL, 0) | O_NONBLOCK );

	if (!stdinIsATTY)
	{
		Com_Printf("tty console mode disabled\n");
		ttycon_on = qfalse;
		stdin_active = qtrue;
		return;
	}

	Field_Clear(&TTY_con);
	tcgetattr (STDIN_FILENO, &TTY_tc);
	TTY_erase = TTY_tc.c_cc[VERASE];
	TTY_eof = TTY_tc.c_cc[VEOF];
	tc = TTY_tc;

	/*
	ECHO: don't echo input characters
	ICANON: enable canonical mode.  This  enables  the  special
	characters  EOF,  EOL,  EOL2, ERASE, KILL, REPRINT,
	STATUS, and WERASE, and buffers by lines.
	ISIG: when any of the characters  INTR,  QUIT,  SUSP,  or
	DSUSP are received, generate the corresponding signal
	*/
	tc.c_lflag &= ~(ECHO | ICANON);

	/*
	ISTRIP strip off bit 8
	INPCK enable input parity checking
	*/
	tc.c_iflag &= ~(ISTRIP | INPCK);
	tc.c_cc[VMIN] = 1;
	tc.c_cc[VTIME] = 0;
	tcsetattr (STDIN_FILENO, TCSADRAIN, &tc);
	ttycon_on = qtrue;
	ttycon_hide = 1; // Mark as hidden, so prompt is shown in CON_Show
	CON_Show();
}

/*
==================
CON_Input
==================
*/
char *CON_Input( void )
{
	// we use this when sending back commands
	static char text[MAX_EDIT_LINE];
	int avail;
	char key;
	unsigned char ukey;
	field_t *history;
	size_t UNUSED_VAR size;
	int i;
	static int needUtf8Bytes = 0;

	if (ConsoleIsPassive) {
		return NULL;
	}

	if(ttycon_on)
	{
		avail = read(STDIN_FILENO, &key, 1);
		if (avail != -1)
		{
			//printf("key... %d  '%d'\n", (unsigned char)key & 0xff, key);

			if (needUtf8Bytes > 0) {
				for (i = 1;  i < 4;  i++) {
					if (TTY_con.xbuffer[TTY_con.cursor].utf8Bytes[i] == '\0') {
						TTY_con.xbuffer[TTY_con.cursor].utf8Bytes[i] = key;
						needUtf8Bytes--;
						break;
					}
				}

				if (needUtf8Bytes <= 0) {
					// got it

					{
						int nbytes;
						qboolean uerror;

						TTY_con.xbuffer[TTY_con.cursor].codePoint = Q_GetCpFromUtf8(TTY_con.xbuffer[TTY_con.cursor].utf8Bytes, &nbytes, &uerror);
					}

					// print the current line (this is differential)
					size = write(STDOUT_FILENO, TTY_con.xbuffer[TTY_con.cursor].utf8Bytes, TTY_con.xbuffer[TTY_con.cursor].numUtf8Bytes);
					TTY_con.cursor++; // next char will always be '\0';
				}

				return NULL;
			}

			// we have something
			// backspace?
			// NOTE TTimo testing a lot of values .. seems it's the only way to get it to work everywhere
			if ((key == TTY_erase) || (key == 127) || (key == 8))
			{
				if (TTY_con.cursor > 0)
				{
					TTY_con.cursor--;
					//TTY_con.buffer[TTY_con.cursor] = '\0';
					TTY_con.xbuffer[TTY_con.cursor].codePoint = '\0';
					TTY_con.xbuffer[TTY_con.cursor].numUtf8Bytes = 1;
					TTY_con.xbuffer[TTY_con.cursor].utf8Bytes[0] = '\0';
					CON_Back();
				}
				return NULL;
			}
			// check if this is a control char
			if ((key) && (key) < ' ' &&  key > '\0')
			{
				//printf("control char xxxxx\n");

				if (key == '\n')
				{
					const char *fieldString;

#ifndef DEDICATED
					// if not in the game explicitly prepend a slash if needed
					if (clc.state != CA_ACTIVE && TTY_con.cursor &&
						TTY_con.xbuffer[0].codePoint != '/' && TTY_con.xbuffer[0].codePoint != '\\')
					{
						memmove(TTY_con.xbuffer + 1, TTY_con.xbuffer, sizeof(TTY_con.xbuffer) - (1 * sizeof(fieldChar_t)));
						TTY_con.xbuffer[0].codePoint = '\\';
						TTY_con.xbuffer[0].numUtf8Bytes = 1;
						TTY_con.xbuffer[0].utf8Bytes[0] = '\\';
						TTY_con.cursor++;
					}

					if (TTY_con.xbuffer[0].codePoint == '/' ||  TTY_con.xbuffer[0].codePoint == '\\') {
						//Q_strncpyz(text, TTY_con.buffer + 1, sizeof(text));
						fieldString = Field_AsStr(&TTY_con, 1, 0);
						Q_strncpyz(text, fieldString, sizeof(text));
					} else if (TTY_con.cursor) {
						if (cl_consoleAsChat->integer) {
							//Com_sprintf(text, sizeof(text), "cmd say %s", TTY_con.buffer);
							Com_sprintf(text, sizeof(text), "cmd say %s", Field_AsStr(&TTY_con, 0, 0));
						} else {
							fieldString = Field_AsStr(&TTY_con, 1, 0);
							Q_strncpyz(text, fieldString, sizeof(text));
						}
					} else {
						text[0] = '\0';
					}

					// push it in history
					Hist_Add(&TTY_con);
					CON_Hide();
					//Com_Printf("%s%s\n", TTY_CONSOLE_PROMPT, TTY_con.buffer);
					Com_Printf("%s%s\n", TTY_CONSOLE_PROMPT, Field_AsStr(&TTY_con, 0, 0));
					Field_Clear(&TTY_con);
					CON_Show();
#else
					// push it in history
					Hist_Add(&TTY_con);
					//Q_strncpyz(text, TTY_con.buffer, sizeof(text));
					fieldString = Field_AsStr(&TTY_con, 0, 0);
					Q_strncpyz(text, fieldString, sizeof(text));
					Field_Clear(&TTY_con);
					key = '\n';
					size = write(STDOUT_FILENO, &key, 1);
					size = write(STDOUT_FILENO, TTY_CONSOLE_PROMPT, strlen(TTY_CONSOLE_PROMPT));
#endif
					return text;
				}
				if (key == '\t')
				{
					CON_Hide();
					Field_AutoComplete( &TTY_con );
					CON_Show();
					return NULL;
				}
				avail = read(STDIN_FILENO, &key, 1);
				if (avail != -1)
				{
					// VT 100 keys
					if (key == '[' || key == 'O')
					{
						avail = read(STDIN_FILENO, &key, 1);
						if (avail != -1)
						{
							switch (key)
							{
								case 'A':
									history = Hist_Prev();
									if (history)
									{
										CON_Hide();
										TTY_con = *history;
										CON_Show();
									}
									tcflush(STDIN_FILENO, TCIFLUSH);
									return NULL;
									break;
								case 'B':
									history = Hist_Next();
									CON_Hide();
									if (history)
									{
										TTY_con = *history;
									} else
									{
										Field_Clear(&TTY_con);
									}
									CON_Show();
									tcflush(STDIN_FILENO, TCIFLUSH);
									return NULL;
									break;
								case 'C':
									return NULL;
								case 'D':
									return NULL;
							}
						}
					}
				}
				Com_DPrintf("droping ISCTL sequence: %d, TTY_erase: %d\n", key, TTY_erase);
				tcflush(STDIN_FILENO, TCIFLUSH);
				return NULL;
			}
			if (TTY_con.cursor >= sizeof(text) - 1)
				return NULL;


			// push regular character and/or check of utf8

			TTY_con.xbuffer[TTY_con.cursor].utf8Bytes[1] = '\0';
			TTY_con.xbuffer[TTY_con.cursor].utf8Bytes[2] = '\0';
			TTY_con.xbuffer[TTY_con.cursor].utf8Bytes[3] = '\0';

			TTY_con.xbuffer[TTY_con.cursor].codePoint = key;
			TTY_con.xbuffer[TTY_con.cursor].utf8Bytes[0] = key;
			TTY_con.xbuffer[TTY_con.cursor].numUtf8Bytes = 1;

			ukey = key;

			// utf8 check
			if (ukey <= 0x7f) {
				// 1 byte utf8, all set
			} else if ((ukey & 0xe0) == 0xc0) {  // 2 bytes
				TTY_con.xbuffer[TTY_con.cursor].numUtf8Bytes = 2;
				needUtf8Bytes = 1;
				return NULL;
			} else if ((ukey & 0xf0) == 0xe0) {  // 3 bytes
				TTY_con.xbuffer[TTY_con.cursor].numUtf8Bytes = 3;
				needUtf8Bytes = 2;
				return NULL;
			} else if ((ukey & 0xf8) == 0xf0) {  // 4 bytes
				TTY_con.xbuffer[TTY_con.cursor].numUtf8Bytes = 4;
				needUtf8Bytes = 3;
				return NULL;
			} else {
				Com_Printf(S_COLOR_YELLOW "CON_Input() invalid byte: %x\n", (unsigned int)ukey);
				return NULL;
			}

			// utf8 1 byte
			needUtf8Bytes = 0;

			TTY_con.cursor++; // next char will always be '\0';
			// print the current line (this is differential)
			size = write(STDOUT_FILENO, &key, 1);
		}

		return NULL;
	}
	else if (stdin_active)
	{
		int     len;
		fd_set  fdset;
		struct timeval timeout;

		FD_ZERO(&fdset);
		FD_SET(STDIN_FILENO, &fdset); // stdin
		timeout.tv_sec = 0;
		timeout.tv_usec = 0;
		if(select (STDIN_FILENO + 1, &fdset, NULL, NULL, &timeout) == -1 || !FD_ISSET(STDIN_FILENO, &fdset))
			return NULL;

		len = read(STDIN_FILENO, text, sizeof(text));
		if (len == 0)
		{ // eof!
			stdin_active = qfalse;
			return NULL;
		}

		if (len < 1)
			return NULL;
		text[len-1] = 0;    // rip off the /n and terminate

		return text;
	}
	return NULL;
}

/*
==================
CON_Print
==================
*/
void CON_Print( const char *msg )
{
	if (!msg[0])
		return;

	if (!ConsoleIsPassive) {
		CON_Hide( );
	}

	if( com_ansiColor && com_ansiColor->integer )
		Sys_AnsiColorPrint( msg );
	else
		fputs( msg, stderr );

	if (ConsoleIsPassive) {
		return;
	}

	if (!ttycon_on) {
		// CON_Hide didn't do anything.
		return;
	}

	// Only print prompt when msg ends with a newline, otherwise the console
	//   might get garbled when output does not fit on one line.
	if (msg[strlen(msg) - 1] == '\n') {
		CON_Show();

		// Run CON_Show the number of times it was deferred.
		while (ttycon_show_overdue > 0) {
			CON_Show();
			ttycon_show_overdue--;
		}
	}
	else
	{
		// Defer calling CON_Show
		ttycon_show_overdue++;
	}
}
