/*
   Copyright (c) 2001, Loki software, inc.
   All rights reserved.

   Redistribution and use in source and binary forms, with or without modification,
   are permitted provided that the following conditions are met:

   Redistributions of source code must retain the above copyright notice, this list
   of conditions and the following disclaimer.

   Redistributions in binary form must reproduce the above copyright notice, this
   list of conditions and the following disclaimer in the documentation and/or
   other materials provided with the distribution.

   Neither the name of Loki software nor the names of its contributors may be used
   to endorse or promote products derived from this software without specific prior
   written permission.

   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
   DIRECT,INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
   (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
   ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

//-----------------------------------------------------------------------------
//
// DESCRIPTION:
// monitoring window for running BSP processes (and possibly various other stuff)

#include "stdafx.h"
#include "watchbsp.h"
#include "feedback.h"

#ifdef _WIN32
#include <winsock2.h>
#endif

#if defined ( __linux__ ) || defined ( __APPLE__ )
#include <sys/time.h>
#define SOCKET_ERROR -1
#endif

#ifdef __APPLE__
#include <unistd.h>
#endif

#include <assert.h>

// Static functions for the SAX callbacks -------------------------------------------------------

// utility for saxStartElement below
static void abortStream( message_info_t *data ){
	g_pParentWnd->GetWatchBSP()->Reset();
	// tell there has been an error
	if ( g_pParentWnd->GetWatchBSP()->HasBSPPlugin() ) {
		g_BSPFrontendTable.m_pfnEndListen( 2 );
	}
	// yeah this doesn't look good.. but it's needed so that everything will be ignored until the stream goes out
	data->ignore_depth = -1;
	data->recurse++;
}

#include "stream_version.h"

static void saxStartElement( message_info_t *data, const xmlChar *name, const xmlChar **attrs ){
	if ( data->ignore_depth == 0 ) {
		if ( data->bGeometry ) {
			// we have a handler
			data->pGeometry->saxStartElement( data, name, attrs );
		}
		else
		{
			if ( strcmp( (char *)name, "q3map_feedback" ) == 0 ) {
				// check the correct version
				// old q3map don't send a version attribute
				// the ones we support .. send Q3MAP_STREAM_VERSION
				if ( !attrs[0] || !attrs[1] || ( strcmp( (char*)attrs[0],"version" ) != 0 ) ) {
					Sys_FPrintf( SYS_ERR, "No stream version given in the feedback stream, this is an old q3map version.\n"
										  "Please turn off monitored compiling if you still wish to use this q3map executable\n" );
					abortStream( data );
					return;
				}
				else if ( strcmp( (char*)attrs[1],Q3MAP_STREAM_VERSION ) != 0 ) {
					Sys_FPrintf( SYS_ERR,
								 "This version of Radiant reads version %s debug streams, I got an incoming connection with version %s\n"
								 "Please make sure your versions of Radiant and q3map are matching.\n", Q3MAP_STREAM_VERSION, (char*)attrs[1] );
					abortStream( data );
					return;
				}
			}
			// we don't treat locally
			else if ( strcmp( (char *)name, "message" ) == 0 ) {
				data->msg_level = atoi( (char *)attrs[1] );
			}
			else if ( strcmp( (char *)name, "polyline" ) == 0 ) {
				// polyline has a particular status .. right now we only use it for leakfile ..
				data->bGeometry = true;
				data->pGeometry = &g_pointfile;
				data->pGeometry->saxStartElement( data, name, attrs );
			}
			else if ( strcmp( (char *)name, "select" ) == 0 ) {
				CSelectMsg *pSelect = new CSelectMsg();
				data->bGeometry = true;
				data->pGeometry = pSelect;
				data->pGeometry->saxStartElement( data, name, attrs );
			}
			else if ( strcmp( (char *)name, "pointmsg" ) == 0 ) {
				CPointMsg *pPoint = new CPointMsg();
				data->bGeometry = true;
				data->pGeometry = pPoint;
				data->pGeometry->saxStartElement( data, name, attrs );
			}
			else if ( strcmp( (char *)name, "windingmsg" ) == 0 ) {
				CWindingMsg *pWinding = new CWindingMsg();
				data->bGeometry = true;
				data->pGeometry = pWinding;
				data->pGeometry->saxStartElement( data, name, attrs );
			}
			else
			{
				Sys_FPrintf( SYS_WRN, "WARNING: ignoring unrecognized node in XML stream (%s)\n", name );
				// we don't recognize this node, jump over it
				// (NOTE: the ignore mechanism is a bit screwed, only works when starting an ignore at the highest level)
				data->ignore_depth = data->recurse;
			}
		}
	}
	data->recurse++;
}

static void saxEndElement( message_info_t *data, const xmlChar *name ) {
	data->recurse--;
	// we are out of an ignored chunk
	if ( data->recurse == data->ignore_depth ) {
		data->ignore_depth = 0;
		return;
	}
	if ( data->bGeometry ) {
		data->pGeometry->saxEndElement( data, name );
		// we add the object to the debug window
		if ( !data->bGeometry ) {
			g_DbgDlg.Push( data->pGeometry );
		}
	}
	if ( data->recurse == data->stop_depth ) {
#ifdef _DEBUG
		Sys_Printf( "Received error msg .. shutting down..\n" );
#endif
		g_pParentWnd->GetWatchBSP()->Reset();
		// tell there has been an error
		if ( g_pParentWnd->GetWatchBSP()->HasBSPPlugin() ) {
			g_BSPFrontendTable.m_pfnEndListen( 2 );
		}
		return;
	}
}

static void saxCharacters( message_info_t *data, const xmlChar *ch, int len ){
	if ( data->bGeometry ) {
		data->pGeometry->saxCharacters( data, ch, len );
	}
	else
	{
		if ( data->ignore_depth != 0 ) {
			return;
		}
		// output the message using the level
		char buf[1024];
		memcpy( buf, ch, len );
		buf[len] = '\0';
		Sys_FPrintf( data->msg_level, "%s", buf );
		// if this message has error level flag, we mark the depth to stop the compilation when we get out
		// we don't set the msg level if we don't stop on leak
		if ( data->msg_level == 3 ) {
			data->stop_depth = data->recurse - 1;
		}
	}
}

static void saxComment( void *ctx, const xmlChar *msg ){
	Sys_Printf( "XML comment: %s\n", msg );
}

static void saxWarning( void *ctx, const char *msg, ... ){
	char saxMsgBuffer[4096];
	va_list args;

	va_start( args, msg );
	vsprintf( saxMsgBuffer, msg, args );
	va_end( args );
	Sys_FPrintf( SYS_WRN, "XML warning: %s\n", saxMsgBuffer );
}

static void saxError( void *ctx, const char *msg, ... ){
	char saxMsgBuffer[4096];
	va_list args;

	va_start( args, msg );
	vsprintf( saxMsgBuffer, msg, args );
	va_end( args );
	Sys_FPrintf( SYS_ERR, "XML error: %s\n", saxMsgBuffer );
}

static void saxFatal( void *ctx, const char *msg, ... ){
	char buffer[4096];

	va_list args;

	va_start( args, msg );
	vsprintf( buffer, msg, args );
	va_end( args );
	Sys_FPrintf( SYS_ERR, "XML fatal error: %s\n", buffer );
}

static xmlSAXHandler saxParser = {
	0, /* internalSubset */
	0, /* isStandalone */
	0, /* hasInternalSubset */
	0, /* hasExternalSubset */
	0, /* resolveEntity */
	0, /* getEntity */
	0, /* entityDecl */
	0, /* notationDecl */
	0, /* attributeDecl */
	0, /* elementDecl */
	0, /* unparsedEntityDecl */
	0, /* setDocumentLocator */
	0, /* startDocument */
	0, /* endDocument */
	(startElementSAXFunc)saxStartElement, /* startElement */
	(endElementSAXFunc)saxEndElement, /* endElement */
	0, /* reference */
	(charactersSAXFunc)saxCharacters, /* characters */
	0, /* ignorableWhitespace */
	0, /* processingInstruction */
	(commentSAXFunc)saxComment, /* comment */
	(warningSAXFunc)saxWarning, /* warning */
	(errorSAXFunc)saxError, /* error */
	(fatalErrorSAXFunc)saxFatal, /* fatalError */
};

// ------------------------------------------------------------------------------------------------

CWatchBSP::~CWatchBSP(){
	Reset();
	if ( m_sBSPName ) {
		delete[] m_sBSPName;
		m_sBSPName = NULL;
	}
	Net_Shutdown();
}

void CWatchBSP::Reset(){
	if ( m_pInSocket ) {
		Net_Disconnect( m_pInSocket );
		m_pInSocket = NULL;
	}
	if ( m_pListenSocket ) {
		Net_Disconnect( m_pListenSocket );
		m_pListenSocket = NULL;
	}
	if ( m_xmlInputBuffer ) {
		xmlFreeParserInputBuffer( m_xmlInputBuffer );
		m_xmlInputBuffer = NULL;
	}
	m_eState = EIdle;
}

bool CWatchBSP::SetupListening(){
#ifdef _DEBUG
	if ( m_pListenSocket ) {
		Sys_Printf( "ERROR: m_pListenSocket != NULL in CWatchBSP::SetupListening\n" );
		return false;
	}
#endif
	Sys_Printf( "Setting up\n" );
	if ( !Net_Setup() ) {
		return false;
	}

	m_pListenSocket = Net_ListenSocket( 39000 );
	if ( m_pListenSocket == NULL ) {
		return false;
	}

	Sys_Printf( "Listening...\n" );
	return true;
}

void CWatchBSP::DoEBeginStep() {
	Reset();
	if ( !SetupListening() ) {
		CString msg;
		msg = "Failed to get a listening socket on port 39000.\nTry running with BSP monitoring disabled if you can't fix this.\n";
		Sys_Printf( msg );
		gtk_MessageBox( g_pParentWnd->m_pWidget, msg, "BSP monitoring", MB_OK | MB_ICONERROR );
		return;
	}
	// set the timer for timeouts and step cancellation
	g_timer_reset( m_pTimer );
	g_timer_start( m_pTimer );

	if ( !m_bBSPPlugin ) {
		Sys_Printf( "=== running BSP command ===\n%s\n", g_ptr_array_index( m_pCmd, m_iCurrentStep ) );

		if ( !Q_Exec( NULL, (char *)g_ptr_array_index( m_pCmd, m_iCurrentStep ), NULL, true ) ) {
			CString msg;
			msg = "Failed to execute the following command: ";
			msg += (char *)g_ptr_array_index( m_pCmd, m_iCurrentStep );
			msg += "\nCheck that the file exists and that you don't run out of system resources.\n";
			Sys_Printf( msg );
			gtk_MessageBox( g_pParentWnd->m_pWidget,  msg, "BSP monitoring", MB_OK | MB_ICONERROR );
			return;
		}
		// re-initialise the debug window
		if ( m_iCurrentStep == 0 ) {
			g_DbgDlg.Init();
		}
	}
	m_eState = EBeginStep;
}

void CWatchBSP::RoutineProcessing(){
	// used for select()
#ifdef _WIN32
	TIMEVAL tout = { 0, 0 };
#endif
#if defined ( __linux__ ) || defined ( __APPLE__ )
	timeval tout;
	tout.tv_sec = 0;
	tout.tv_usec = 0;
#endif

	switch ( m_eState )
	{
	case EBeginStep:
		// timeout: if we don't get an incoming connection fast enough, go back to idle
		if ( g_timer_elapsed( m_pTimer, NULL ) > g_PrefsDlg.m_iTimeout ) {
			gtk_MessageBox( g_pParentWnd->m_pWidget,  "The connection timed out, assuming the BSP process failed\nMake sure you are using a networked version of Q3Map?\nOtherwise you need to disable BSP Monitoring in prefs.", "BSP process monitoring", MB_OK );
			Reset();
			if ( m_bBSPPlugin ) {
				// status == 1 : didn't get the connection
				g_BSPFrontendTable.m_pfnEndListen( 1 );
			}
			return;
		}
#ifdef _DEBUG
		// some debug checks
		if ( !m_pListenSocket ) {
			Sys_Printf( "ERROR: m_pListenSocket == NULL in CWatchBSP::RoutineProcessing EBeginStep state\n" );
			return;
		}
#endif
		// we are not connected yet, accept any incoming connection
		m_pInSocket = Net_Accept( m_pListenSocket );
		if ( m_pInSocket ) {
			Sys_Printf( "Connected.\n" );
			// prepare the message info struct for diving in
			memset( &m_message_info, 0, sizeof( message_info_s ) );
			// a dumb flag to make sure we init the push parser context when first getting a msg
			m_bNeedCtxtInit = true;
			m_eState = EWatching;
		}
		break;
	case EWatching:
#ifdef _DEBUG
		// some debug checks
		if ( !m_pInSocket ) {
			Sys_Printf( "ERROR: m_pInSocket == NULL in CWatchBSP::RoutineProcessing EWatching state\n" );
			return;
		}
#endif
		// select() will identify if the socket needs an update
		// if the socket is identified that means there's either a message or the connection has been closed/reset/terminated
		fd_set readfds;
		int ret;
		FD_ZERO( &readfds );
		FD_SET( ( (unsigned int)m_pInSocket->socket ), &readfds );
		// from select man page:
		// n is the highest-numbered descriptor in any of the three sets, plus 1
		// (no use on windows)
		ret = select( m_pInSocket->socket + 1, &readfds, NULL, NULL, &tout );
		if ( ret == SOCKET_ERROR ) {
			Sys_Printf( "WARNING: SOCKET_ERROR in CWatchBSP::RoutineProcessing\n" );
			Sys_Printf( "Terminating the connection.\n" );
			Reset();
			return;
		}
#ifdef _DEBUG
		if ( ret == -1 ) {
			// we are non-blocking?? we should never get timeout errors
			Sys_Printf( "WARNING: unexpected timeout expired in CWatchBSP::Processing\n" );
			Sys_Printf( "Terminating the connection.\n" );
			Reset();
			return;
		}
#endif
		if ( ret == 1 ) {
			// the socket has been identified, there's something (message or disconnection)
			// see if there's anything in input
			ret = Net_Receive( m_pInSocket, &msg );
			if ( ret > 0 ) {
				//        unsigned int size = msg.size; //++timo just a check
				strcpy( m_xmlBuf, NMSG_ReadString( &msg ) );
				if ( m_bNeedCtxtInit ) {
					m_xmlParserCtxt = NULL;
					m_xmlParserCtxt = xmlCreatePushParserCtxt( &saxParser, &m_message_info, m_xmlBuf, strlen( m_xmlBuf ), NULL );
					if ( m_xmlParserCtxt == NULL ) {
						Sys_FPrintf( SYS_ERR, "Failed to create the XML parser (incoming stream began with: %s)\n", m_xmlBuf );
						Reset();
					}
					m_bNeedCtxtInit = false;
				}
				else
				{
					xmlParseChunk( m_xmlParserCtxt, m_xmlBuf, strlen( m_xmlBuf ), 0 );
				}
			}
			else
			{
				// error or connection closed/reset
				// NOTE: if we get an error down the XML stream we don't reach here
				Net_Disconnect( m_pInSocket );
				m_pInSocket = NULL;
				Sys_Printf( "Connection closed.\n" );
				if ( m_bBSPPlugin ) {
					Reset();
					// let the BSP plugin know that the job is done
					g_BSPFrontendTable.m_pfnEndListen( 0 );
					return;
				}
				// move to next step or finish
				m_iCurrentStep++;
				if ( m_iCurrentStep < m_pCmd->len ) {
					DoEBeginStep();
				}
				else
				{
					// release the GPtrArray and the strings
					if ( m_pCmd != NULL ) {
						for ( m_iCurrentStep = 0; m_iCurrentStep < m_pCmd->len; m_iCurrentStep++ )
						{
							delete[] (char *)g_ptr_array_index( m_pCmd, m_iCurrentStep );
						}
						g_ptr_array_free( m_pCmd, false );
					}
					m_pCmd = NULL;
					// launch the engine .. OMG
					if ( g_PrefsDlg.m_bRunQuake ) {
						// do we enter sleep mode before?
						if ( g_PrefsDlg.m_bDoSleep ) {
							Sys_Printf( "Going into sleep mode..\n" );
							g_pParentWnd->OnSleep();
						}
						Sys_Printf( "Running engine...\n" );
						Str cmd;
						// build the command line
						cmd = g_pGameDescription->mEnginePath.GetBuffer();
						// this is game dependant
						if ( !strcmp( ValueForKey( g_qeglobals.d_project_entity, "gamemode" ),"mp" ) ) {
							// MP
							cmd += g_pGameDescription->mMultiplayerEngine.GetBuffer();
						}
						else
						{
							// SP
							cmd += g_pGameDescription->mEngine.GetBuffer();
						}
#ifdef _WIN32
						// NOTE: we are using unix pathnames and CreateProcess doesn't like / in the program path
						// FIXME: This isn't true anymore, doesn't it?
						FindReplace( cmd, "/", "\\" );
#endif
						Str cmdline;
						if ( g_pGameDescription->quake2 ) {
							cmdline = ". +exec radiant.cfg +map ";
							cmdline += m_sBSPName;
						}
						else
						{
							cmdline = "+set sv_pure 0 ";
							// TTimo: a check for vm_* but that's all fine
							//cmdline = "+set sv_pure 0 +set vm_ui 0 +set vm_cgame 0 +set vm_game 0 ";
							if ( *ValueForKey( g_qeglobals.d_project_entity, "gamename" ) != '\0' ) {
								cmdline += "+set fs_game ";
								cmdline += ValueForKey( g_qeglobals.d_project_entity, "gamename" );
								cmdline += " ";
							}
							//!\todo Read the start-map args from a config file.
							if ( g_pGameDescription->mGameFile == "wolf.game" ) {
								if ( !strcmp( ValueForKey( g_qeglobals.d_project_entity, "gamemode" ),"mp" ) ) {
									// MP
									cmdline += "+devmap ";
									cmdline += m_sBSPName;
								}
								else
								{
									// SP
									cmdline += "+set nextmap \"spdevmap ";
									cmdline += m_sBSPName;
									cmdline += "\"";
								}
							}
							else
							{
								cmdline += "+devmap ";
								cmdline += m_sBSPName;
							}
						}

						Sys_Printf( "%s %s\n", cmd.GetBuffer(), cmdline.GetBuffer() );

						// execute now
						if ( !Q_Exec( cmd.GetBuffer(), (char *)cmdline.GetBuffer(), g_pGameDescription->mEnginePath.GetBuffer(), false ) ) {
							CString msg;
							msg = "Failed to execute the following command: ";
							msg += cmd; msg += cmdline;
							Sys_Printf( msg );
							gtk_MessageBox( g_pParentWnd->m_pWidget,  msg, "BSP monitoring", MB_OK | MB_ICONERROR );
						}
					}
					Reset();
				}
			}
		}
		break;
	default:
		break;
	}
}

void CWatchBSP::DoMonitoringLoop( GPtrArray *pCmd, char *sBSPName ){
	if ( m_sBSPName ) {
		delete[] m_sBSPName;
	}
	m_sBSPName = sBSPName;
	if ( m_eState != EIdle ) {
		Sys_Printf( "WatchBSP got a monitoring request while not idling...\n" );
		// prompt the user, should we cancel the current process and go ahead?
		if ( gtk_MessageBox( g_pParentWnd->m_pWidget,  "I am already monitoring a BSP process.\nDo you want me to override and start a new compilation?",
							 "BSP process monitoring", MB_YESNO ) == IDYES ) {
			// disconnect and set EIdle state
			Reset();
		}
	}
	m_pCmd = pCmd;
	m_iCurrentStep = 0;
	DoEBeginStep();
}

void CWatchBSP::ExternalListen(){
	m_bBSPPlugin = true;
	DoEBeginStep();
}

// the part of the watchbsp interface we export to plugins
// NOTE: in the long run, the whole watchbsp.cpp interface needs to go out and be handled at the BSP plugin level
// for now we provide something really basic and limited, the essential is to have something that works fine and fast (for 1.1 final)
void WINAPI QERApp_Listen(){
	// open the listening socket
	g_pParentWnd->GetWatchBSP()->ExternalListen();
}
