//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose: 
//
// $NoKeywords: $
//
//=============================================================================//
// CTextConsoleUnix.cpp: Unix implementation of the TextConsole class.
//
//////////////////////////////////////////////////////////////////////

#ifndef _WIN32

#include <sys/ioctl.h>

#include "TextConsoleUnix.h"
#include "tier0/icommandline.h"
#include "tier1/utllinkedlist.h"
#include "filesystem.h"
#include "../thirdparty/libedit-3.1/src/histedit.h"
#include "tier0/vprof.h"

#define CONSOLE_LOG_FILE "console.log"

static pthread_mutex_t g_lock;
static pthread_t g_threadid = (pthread_t)-1;
CUtlLinkedList< CUtlString > g_Commands;
static volatile int g_ProcessingCommands = false;

#if defined( LINUX )

// Dynamically load the libtinfo stuff. On servers without a tty attached,
//  we get to skip all of these dependencies on servers without ttys.
#define TINFO_SYM(rc, fn, params, args, ret)	\
	typedef rc (*DYNTINFOFN_##fn) params;		\
	static DYNTINFOFN_##fn g_TINFO_##fn;		\
	extern "C" rc fn params						\
	{											\
		ret g_TINFO_##fn args;					\
	}

TINFO_SYM(char *, tgoto,    (char *string, int x, int y), (string, x, y), return);
TINFO_SYM(int,    tputs,    (const char *str, int affcnt, int (*putc)(int)), (str, affcnt, putc), return);
TINFO_SYM(int,    tgetflag, (char *id), (id), return);
TINFO_SYM(int,    tgetnum,  (char *id), (id), return);
TINFO_SYM(int,    tgetent,  (char *bufp, const char *name), (bufp, name), return);
TINFO_SYM(char *, tgetstr,  (char *id, char **area), (id, area), return);

#endif // LINUX

//----------------------------------------------------------------------------------------------------------------------
// init_tinfo_functions
//----------------------------------------------------------------------------------------------------------------------
static bool init_tinfo_functions()
{
#if !defined( LINUX )
    return true;
#else
	static void *s_ncurses_handle = NULL;

	if ( !s_ncurses_handle )
	{
		// Long time ago, ncurses was two libraries. So if libtinfo fails, try libncurses.
		static const char *names[] = { "libtinfo.so.5", "libncurses.so.5" };

		for ( int i = 0; !s_ncurses_handle && ( i < ARRAYSIZE( names ) ); i++ )
		{
			bool bFailed = true;
			s_ncurses_handle = dlopen( names[i], RTLD_NOW );

			if ( s_ncurses_handle )
			{
				bFailed = false;
#define LOADTINFOFUNC(_handle, _func, _failed) 									\
			do { 																\
				g_TINFO_##_func = ( DYNTINFOFN_##_func )dlsym(_handle, #_func); \
				if ( !g_TINFO_##_func) 											\
					_failed = true; 											\
			} while (0)

				LOADTINFOFUNC( s_ncurses_handle, tgoto, bFailed );
				LOADTINFOFUNC( s_ncurses_handle, tputs, bFailed );
				LOADTINFOFUNC( s_ncurses_handle, tgetflag, bFailed );
				LOADTINFOFUNC( s_ncurses_handle, tgetnum, bFailed );
				LOADTINFOFUNC( s_ncurses_handle, tgetent, bFailed );
				LOADTINFOFUNC( s_ncurses_handle, tgetstr, bFailed );
#undef LOADTINFOFUNC
			}

			if ( bFailed )
				s_ncurses_handle = NULL;
		}

		if ( !s_ncurses_handle )
		{
			fprintf( stderr, "\nWARNING: Failed to load 32-bit libtinfo.so.5 or libncurses.so.5.\n"
							 "  Please install (lib32tinfo5 / ncurses-libs.i686 / equivalent) to enable readline.\n\n");
		}
	}

	return !!s_ncurses_handle;
#endif // LINUX
}

static unsigned char editline_complete( EditLine *el, int ch __attribute__((__unused__)) )
{
	static const char *s_cmds[] =
	{
		"cvarlist ",
		"find ",
		"help ",
		"maps ",
		"nextlevel",
		"quit",
		"status",
		"sv_cheats ",
		"tf_bot_quota ",
		"toggle ",
		"sv_dump_edicts",
#ifdef STAGING_ONLY
		"tf_bot_use_items ",
#endif
	};

	const LineInfo *lf = el_line(el);
	const char *cmd = lf->buffer;
	size_t len = lf->cursor - cmd;

	if ( len > 0 )
	{
		for (int i = 0; i < ARRAYSIZE(s_cmds); i++)
		{
			if ( len > strlen( s_cmds[i] ) )
				continue;

			if ( !Q_strncmp( cmd, s_cmds[i], len ) )
			{ 
				if ( el_insertstr( el, s_cmds[i] + len ) == -1 )
					return CC_ERROR;
				else 
					return CC_REFRESH;
			}
		}
	}

	return CC_ERROR; 
}

static const char *editline_prompt( EditLine *e )
{
	// Something like: "\1\033[7m\1Srcds$\1\033[0m\1 "
	static const char *szPrompt = getenv( "SRCDS_PROMPT" );
	return szPrompt ? szPrompt : "";
}

static void editline_cleanup_handler( void *arg )
{
	if ( arg )
	{
		EditLine *el = (EditLine *)arg;
		el_end( el );
	}
}

static bool add_command( const char *cmd, int cmd_len )
{
	if ( cmd )
	{
		tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ ); 

		// Trim trailing whitespace.
		while ( ( cmd_len > 0 ) && isspace( cmd[ cmd_len - 1 ] ) )
			cmd_len--;

		if ( cmd_len > 0 )
		{
			pthread_mutex_lock( &g_lock );
			if ( g_Commands.Count() < 32 )
			{
				CUtlString szCommand( cmd, cmd_len );
				g_Commands.AddToTail( szCommand );

				g_ProcessingCommands = true;
			}
			pthread_mutex_unlock( &g_lock );

			// Wait a bit until we've processed the command we added.
			for ( int i = 0; i < 6; i++ )
			{
				while ( g_ProcessingCommands )
					usleep( 500 );
			}

			return true;
		}
	}

	return false;
}

static void *editline_threadproc( void *arg )
{
	HistEvent ev;
	EditLine *el;
	History *myhistory;
	FILE *tty = (FILE *)arg;

	ThreadSetDebugName( "libedit" );

	// Set up state
	el = el_init( "srcds_linux", stdin, tty, stderr );
	el_set( el, EL_PROMPT, &editline_prompt );
	el_set( el, EL_EDITOR, "emacs" ); // or "vi"

	// Hitting Ctrl+R will reset prompt.
	el_set( el, EL_BIND, "^R", "ed-redisplay", NULL );

	/* Add a user-defined function  */
	el_set( el, EL_ADDFN, "ed-complete", "Complete argument", editline_complete );
	/* Bind tab to it       */
	el_set( el, EL_BIND, "^I", "ed-complete", NULL );

	// Init history.
	myhistory = history_init();
	if (myhistory == 0)
	{
		fprintf( stderr, "history could not be initialized\n" );
		g_threadid = (pthread_t)-1;
		return (void *)-1;
	}

	// History size.
	history( myhistory, &ev, H_SETSIZE, 800 );

	// History callback.
	el_set( el, EL_HIST, history, myhistory );

	// Source user's defaults.
	el_source( el, NULL );

	pthread_cleanup_push( editline_cleanup_handler, el );

	while ( g_threadid != (pthread_t)-1 )
	{
		// count is the number of characters read.
		// line is a const char* of our command line with the tailing \n
		int count;
		const char *line = el_gets( el, &count );

		if ( add_command( line, count ) )
		{
			// Add command to history.
			history( myhistory, &ev, H_ENTER, line );
		}
	}

	pthread_cleanup_pop( 0 );

	// Clean up...
	history_end( myhistory );
	el_end( el );
	return NULL;
}

static void *fgets_threadproc( void *arg )
{
	pthread_cleanup_push( editline_cleanup_handler, NULL );

	while ( g_threadid != (pthread_t)-1 )
	{
		char cmd[ 512 ];

		if ( fgets( cmd, sizeof( cmd ), stdin ) )
		{
			cmd[ sizeof(cmd) - 1 ] = 0;
			add_command( cmd, strlen( cmd ) );
		}
	}

	pthread_cleanup_pop( 0 );
	return NULL;
}

bool CTextConsoleUnix::Init()
{
	if( g_threadid != (pthread_t)-1 )
	{
		Assert( !"CTextConsoleUnix can only handle a single thread!" );
		return false;
	}

	pthread_mutex_init( &g_lock, NULL );

	// This code is for echo-ing key presses to the connected tty
	//   (which is != STDOUT)
	if ( isatty( STDIN_FILENO ) )
	{
		const char *termid_str = ctermid( NULL );

		m_tty = fopen( termid_str, "w+" );
		if ( !m_tty )
		{
			fprintf( stderr, "WARNING: Unable to open tty(%s) for output.\n", termid_str );
			m_tty = stdout;
		}

		void *(*terminal_threadproc) (void *) = editline_threadproc;
		if ( !init_tinfo_functions() )
			terminal_threadproc = fgets_threadproc;

		if ( pthread_create( &g_threadid, NULL, terminal_threadproc, (void *)m_tty ) != 0 )
		{
			g_threadid = (pthread_t)-1;
			fprintf( stderr, "WARNING: pthread_create failed: %s.\n", strerror(errno) ); 
		}
	}
	else
	{
		m_tty = fopen( "/dev/null", "w+" );
		if ( !m_tty )
			m_tty = stdout;
	}

	m_bConDebug = CommandLine()->FindParm( "-condebug" ) != 0;
	if ( m_bConDebug && CommandLine()->FindParm( "-conclearlog" ) )
		g_pFullFileSystem->RemoveFile( CONSOLE_LOG_FILE, "GAME" );

	return CTextConsole::Init();
}

void CTextConsoleUnix::ShutDown()
{
	if ( g_threadid != (pthread_t)-1 )
	{
		void *status = NULL;
		pthread_t tid = g_threadid;

		g_threadid = (pthread_t)-1;
#if defined ( ANDROID )
		pthread_kill( tid, SIGUSR1 );
#else
		pthread_cancel( tid );
#endif
		pthread_join( tid, &status );
	}

	pthread_mutex_destroy( &g_lock ); 
}

void CTextConsoleUnix::Print( char * pszMsg )
{
	int nChars = strlen( pszMsg );

	if ( nChars > 0 )
	{
		if ( m_bConDebug )
		{
			FileHandle_t fh = g_pFullFileSystem->Open( CONSOLE_LOG_FILE, "a" );
			if ( fh != FILESYSTEM_INVALID_HANDLE )
			{
				g_pFullFileSystem->Write( pszMsg, nChars, fh );
				g_pFullFileSystem->Close( fh );
			}
		}

		fwrite( pszMsg, 1, nChars, m_tty );
	}
}

void CTextConsoleUnix::SetTitle( char *pszTitle )
{
}

void CTextConsoleUnix::SetStatusLine( char *pszStatus )
{
}

void CTextConsoleUnix::UpdateStatus()
{
}

char *CTextConsoleUnix::GetLine( int index, char *buf, int buflen )
{
	if ( g_threadid != (pthread_t)-1 )
	{
		if ( g_Commands.Count() > 0 )
		{
			pthread_mutex_lock( &g_lock );

			const CUtlString& psCommand = g_Commands[ g_Commands.Head() ];
			V_strncpy( buf, psCommand.Get(), buflen );
			g_Commands.Remove( g_Commands.Head() );

			pthread_mutex_unlock( &g_lock );
			return buf;
		}
		else if ( index == 0 )
		{
			// We're being asked for the first command. Must be a new frame.
			//  Reset the processed commands global.
			g_ProcessingCommands = false;
		}
	}

	return NULL;
}

int CTextConsoleUnix::GetWidth()
{
	int nWidth = 0;
	struct winsize ws;

	if ( ioctl( STDOUT_FILENO, TIOCGWINSZ, &ws ) == 0 )
		nWidth = (int)ws.ws_col;

	if ( nWidth <= 1 )
		nWidth = 80;

	return nWidth;
}

#endif // !_WIN32