/*
** This program 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 1, or (at your option)
** any later version.

** This program 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 this program; if not, write to the Free Software
** Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

/*
 * Author : Alexandre Parenteau <aubonbeurre@hotmail.com> --- December 1997
 */

/*
 * AppGlue.cpp --- glue code to access CVS services
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "stdafx.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>


#ifdef TARGET_OS_MAC
#	include <TextUtils.h>
#	include <Strings.h>
#	include <Dialogs.h>
#	include <unistd.h>
#	include <errno.h>
#	include <console.h>
#	include <ctype.h>
#	include "mktemp.h"
#	include "dll_process.h"
#	include "AppGlue.mac.h"
#	include "MacMisc.h"
#endif /* TARGET_OS_MAC */

#ifdef qMacCvsPP
#	include "MacCvsApp.h"
#	include "LogWindow.h"
#	include "MacBinEncoding.h"
#	include "MacCvsAE.h"
#endif /* qMacCvsPP */

#ifdef WIN32
#	include "wincvs.h"
#	include "PromptFiles.h"
#	include "BrowseFileView.h"
#	include "wincvs_winutil.h"

#	include <process.h>
#	include <direct.h>

#	ifdef _DEBUG
#	define new DEBUG_NEW
#	undef THIS_FILE
	static char THIS_FILE[] = __FILE__;
#	endif
#endif /* WIN32 */

#include "cvsgui_process.h"

#if qUnix
#	include "UCvsApp.h"
#	include "UCvsFiles.h"
#	include <ctype.h>
#endif /* qUnix */

#ifdef HAVE_ERRNO_H
#	include <errno.h>
#endif

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

#include "Authen.h"
#include "CvsPrefs.h"
#include "AppGlue.h"
#include "AppConsole.h"
#include "GetPassword.h"
#include "AskYesNo.h"
#include "CvsArgs.h"
#include "FileTraversal.h"
#include "CvsCommands.h"
#include "cvsgui_i18n.h"

/// Console to which the current cvs process writes
CCvsConsole* gConsole = 0L;

/// Last cvs command exit code
static int sLastExitCode;

/// Temporary files list
CTmpFiles sTmpList;

#ifdef _MSC_VER
#	define CVS_EXTERN_C extern "C"
#else
#	define CVS_EXTERN_C
#endif

static CvsProcessCallbacks sCallTable = {
	glue_consoleout,
	glue_consoleerr,
	glue_getenv,
	glue_exit
};

/*!
	Print the cvs command exit code
	\param code Exit code to print
*/
CVS_EXTERN_C void glue_exit(int code)
{
	if( !gCvsPrefs.IsTclFileRunning() )
		cvs_out(_i18n("\n***** CVS exited normally with code %d *****\n\n"), code);

	sLastExitCode = code;
}

/*!
	Send stdout of the cvs command to the console
	\param txt Text to be send to the console
	\param len Text lenght
	\return The printed text lenght
*/
CVS_EXTERN_C long glue_consoleout(char* txt, long len)
{
	if( gConsole != 0L )
		return gConsole->cvs_out(txt, len);
	
	cvs_outstr(txt, len);

	return len;
}

/*!
	Send stderr of the cvs command to the console
	\param txt Text to be send to the console
	\param len Text lenght
	\return The printed text lenght
*/
CVS_EXTERN_C long glue_consoleerr(char* txt, long len)
{
	if( gConsole != 0L )
		return gConsole->cvs_err(txt, len);
	
	cvs_errstr(txt, len);

	return len;
}

/*!
	Send stdin of the console to the cvs command
	\param txt Text to be send to the cvs command
	\param len Text lenght
	\return The sent text lenght
*/
CVS_EXTERN_C long glue_consolein(char* txt, long len)
{
	return 0;
}

#ifdef TARGET_OS_MAC
#	define isGlobalEnvVariable(name) ( \
		strcmp(name, "CVS_PSERVER_PORT") == 0 || \
		strcmp(name, "CVS_RCMD_PORT") == 0 || \
		strcmp(name, "CVS_SERVER") == 0 || \
		strcmp(name, "CVS_RSH") == 0 || \
		strcmp(name, "CVS_RSA_IDENTITY") == 0 || \
		strcmp(name, "HOME") == 0 || \
		strcmp(name, "ISO8859") == 0 || \
		strcmp(name, "IC_ON_TXT") == 0 || \
		strcmp(name, "MAC_DEFAULT_RESOURCE_ENCODING") == 0 || \
		strcmp(name, "MAC_BINARY_TYPES_SINGLE") == 0 || \
		strcmp(name, "MAC_BINARY_TYPES_HQX") == 0 || \
		strcmp(name, "MAC_BINARY_TYPES_PLAIN") == 0 || \
		strcmp(name, "CVSUSEUNIXLF") == 0 || \
		strcmp(name, "DIRTY_MCP") == 0 \
		)
#endif /* TARGET_OS_MAC */

/*!
	Set the environmental variables for the cvs command
	\param name Environmental variable name
	\return The environmental variable value as const char* 
	\note Make sure that the returned pointer doesn't get deallocated prematurely
*/
CVS_EXTERN_C const char* glue_getenv(char* name)
{
	if( name == NULL )
		return NULL;
	
#ifdef WIN32
	CWincvsApp* app = (CWincvsApp*)AfxGetApp();
	if( !app )
	{
		ASSERT(FALSE);	// need an application object
		return NULL;
	}

	if( ::GetCurrentThreadId() != app->m_nThreadID &&
		(strcmp(name, "CVS_GETPASS") == 0 || strcmp(name, "CVSLIB_YESNO") == 0 ||
		strcmp(name, "HOME") == 0) )
	{
		// synchonize it since the thread calls and
		// we call some MFC objects in it (prompt
		// password, get path...)
		CCvsThreadLock threadLock;

		return app->GetEnvMainThread(name);
	}
#endif /* WIN32 */

#ifdef TARGET_OS_MAC
	char* result;
	if( (result = ae_getenv(name)) != (char*)-1 )
	{
		if( result == 0L && isGlobalEnvVariable(name) )
		{
			// in this case only we let continue
			// to get the MacCVS settings. So the CWCVS
			// users (or any AppleEvent user) can set
			// these variables in MacCVS but still can overide
			// them using Apple Events.
		}
		else
			return result;
	}
#endif /* TARGET_OS_MAC */
	
	// Give a chance for the new AuthenModel to react to the getenv callback
	AuthenModel* curModel = AuthenModel::GetInstance(gAuthen.kind());
	std::vector<AuthenModel*>& allInstances = AuthenModel::GetAllInstances();
	std::vector<AuthenModel*>::const_iterator i;

	if( curModel != 0L )
	{
		const char* res = curModel->OnGetEnv(name);
		if( res != 0L )
			return res;
	}

	for(i = allInstances.begin(); i != allInstances.end(); ++i)
	{
		AuthenModel* model = *i;

		if( model == curModel )
			continue;

		const char* res = model->OnGetEnv(name);
		if( res != 0L )
			return res;
	}

	if( strcmp(name, "USER") == 0
#ifdef WIN32
		// I do that (and see the "STRATA_HACK" in
		// subr.c) because people get tired on Windows
		// to have to check their Windows login :
		// this is only for .rhosts authentication.
		|| strcmp(name, "LOGNAME") == 0
#endif /* WIN32 */
		)
	{
		static char user[64];
		
		// extract from the cvsroot
		const char* ccvsroot = gCvsPrefs;
		ccvsroot = Authen::skiptoken(ccvsroot);
		char* login = strchr(ccvsroot, '@');
		if( login == NULL )
		{
			// for WIN32/gcvs this means the CVSROOT is local
#ifdef TARGET_OS_MAC
			cvs_err(_i18n("MacCvs: Cannot extract login from cvsroot %s !\n"), ccvsroot);
#endif /* TARGET_OS_MAC */
			return NULL;
		}

		int length = login - ccvsroot;
		memcpy(user, ccvsroot, length * sizeof(char));
		user[length] = '\0';

		return user;
	}
	else if( strcmp(name, "CVSROOT") == 0 )
	{
		static CStr root;
		
		// extract from the cvsroot
		const char* ccvsroot = gCvsPrefs;
		ccvsroot = Authen::skiptoken(ccvsroot);
		root = gAuthen.token();
		root << ccvsroot;

		return root;
	}
	else if( strcmp(name, "CVSREAD") == 0 )
	{
		return gCvsPrefs.CheckoutRO() ? (char*)"1" : 0L;
	}
	else if( strncmp(name, "CVS_GETPASS", strlen("CVS_GETPASS")) == 0 )
	{
		UStr str(_i18n("Enter the password:"));
		char *tmp;
		if( (tmp = strchr(name, '=')) != 0L )
		{
			str = ++tmp;
		}

		return CompatGetPassword(str);
	}
	else if( strcmp(name, "CVSLIB_YESNO") == 0 )
	{
		return (char*)AskYesNo();
	}
	else if( strcmp(name, "DIRTY_MCP") == 0 )
	{
		return gCvsPrefs.DirtySupport() ? (char*)"yes" : (char*)"no";
	}
#ifdef WIN32
	else if( strcmp(name, "HOME") == 0 )
	{
		// prompt for home the first time
		if( gCvsPrefs.Home() == 0L )
		{
			char oldpath[_MAX_PATH];
			getcwd(oldpath, _MAX_PATH-1);
			
			CStr home;
			if( !BrowserGetDirectory(_i18n("Select your home directory"), home) )
			{
				chdir(oldpath);
				return 0L;
			}

			gCvsPrefs.SetHome(home);
			gCvsPrefs.save();
			chdir(oldpath);
		}

		// Windows 98 doesn't like C:\/.cvspass
		static CStr home;
		home = gCvsPrefs.Home();
		if( home.endsWith(kPathDelimiter) )
		{
			home[home.length() - 1] = '\0';
		}

		return home;
	}
	else if( strcmp(name, "CVSUSEUNIXLF") == 0 )
	{
		return gCvsPrefs.UnixLF() ? (char*)"1" : 0L;
	}
#endif /*WIN32 */
#if TARGET_RT_MAC_MACHO
	else if( strcmp(name, "CVS_MACLF") == 0 )
	{
		return gCvsPrefs.MacLF() ? (char*)"1" : 0L;
	}
#endif
#ifdef TARGET_OS_MAC
	else if( strcmp(name, "ISO8859") == 0 )
	{
		static char iso[6];
		
		if( gCvsPrefs.IsoConvert() == ISO8559_none )
			return 0L;
		
		sprintf(iso, "%d", (int)gCvsPrefs.IsoConvert());
		return iso;
	}
	else if( strcmp(name, "IC_ON_TXT") == 0 )
	{
		return gCvsPrefs.ICTextOption() ? (char *)"1" : 0L;
	}
	else if( strcmp(name, "MAC_DEFAULT_RESOURCE_ENCODING") == 0 )
	{
		switch(gCvsPrefs.MacBinEncoding())
		{
			case MAC_HQX:
				return (char*)"HQX";
				break;
			case MAC_APPSINGLE:
				return (char*)"AppleSingle";
				break;
		}
		return 0L;
	}
	else if( strcmp(name, "MAC_BINARY_TYPES_SINGLE") == 0 )
	{
		return (char*)MacBinMaps::GetEnvTypes(MAC_APPSINGLE);
	}
	else if( strcmp(name, "MAC_BINARY_TYPES_HQX") == 0 )
	{
		return (char*)MacBinMaps::GetEnvTypes(MAC_HQX);
	}
	else if( strcmp(name, "MAC_BINARY_TYPES_PLAIN") == 0 )
	{
		return (char*)MacBinMaps::GetEnvPlainTypes();
	}
#endif /* TARGET_OS_MAC */
	else 
	{
		char* res;
		res = getenv(name);
		return res;
	}
	
	return NULL;
}

/*!
	Launch cvs command
	\param test cvs testing
	\param path Cvs binary file
	\param argc Arguments count
	\param argv Arguments
	\param console Console
	\param cvsfile Cvs binary file
	\return The process return error code
*/
int launchCVS(int test, const char* path, int argc, char* const* argv, CCvsConsole* console /*= 0L*/, const char *cvsfile /*= 0L*/ )
{
	CAuthenSetUp setupEnv;
	CvsProcessStartupInfo startupInfo;

	startupInfo.hasTty = ( !test ? gCvsPrefs.CvsConsole() : true ); // test cvs has always tty (prevent printing cvs version info on console
	startupInfo.currentDirectory = path;
	
	gConsole = console;

	CStr cvscmd;

#ifdef WIN32
	CWincvsApp* app = (CWincvsApp*)AfxGetApp();
#elif qUnix
	UCvsApp* app = UCvsApp::gApp;
#elif TARGET_RT_MAC_MACHO
	CMacCvsApp* app = CMacCvsApp::gApp;
#endif
	
	if( app->IsCvsRunning() && cvs_process_is_active(app->GetCvsProcess()) ) {
		cvs_err(_i18n("Error : cvs is already running !\n"));
		return -1;
	}
	
#if qUnix || TARGET_RT_MAC_MACHO
	if(path != 0L && chdir(path) != 0) {
		cvs_err(("Error : cannot chdir to %s (error %d)!\n"), path, errno);
	}
#endif // qUnix
	
	app->SetCvsStopping(false);
	
	if( !test ) {
		gCvsPrefs.WhichCvs(cvscmd, gCvsPrefs.UseAltCvs());
		sLastExitCode = 0;
	} else {
		cvscmd = cvsfile;
		sLastExitCode = errNoCvsguiProtocol;
	}
	
	struct stat sb;

	if( stat(cvscmd, &sb) == -1 ||
#if qUnix || TARGET_RT_MAC_MACHO
		 S_ISLNK(sb.st_mode) ||
#endif
		 !(S_ISREG(sb.st_mode)) )
	{
		cvs_err(_i18n("Unable to find the cvs process in : (error %d)\n"), errno);
		cvs_err("1) %s\n", (const char*)cvscmd);
	}

	CvsProcess* proc = cvs_process_run(cvscmd, argc - 1, (char**)argv + 1, &sCallTable, &startupInfo);
	if( proc == 0L )
	{
		cvs_err(_i18n("Unable to initialize the cvs process (error %d)\n"), errno);
		cvs_err(_i18n("The cvs used is : %s\n"), (const char*)cvscmd);
	}
	else 
	{
#ifdef TARGET_RT_MAC_MACHO
		UInt32 ticker = ::TickCount();
#endif

		app->SetCvsRunning(true);
		app->SetCvsProcess(proc);
#if defined(WIN32) || defined(qUnix)
		app->PeekPumpAndIdle(true); 
#endif
#ifndef qUnix
		if( console != 0L )
#endif
		{			
			time_t start = time(NULL);
		
			while( TRUE )
			{
#if defined(WIN32) || defined(qUnix)
				app->PeekPumpAndIdle(false); 
#endif

#if TARGET_RT_MAC_MACHO
				if( (::TickCount () - ticker) > 10 )
				{
					glue_eventdispatcher(0L);
					ticker = ::TickCount ();
				}
#endif
				cvs_process_give_time();
			
				if( !cvs_process_is_active(proc) )
					break;
				
				if ( test && ( (time(NULL) - start) > 10 ) )
				{	
					cvs_err(_i18n("testing cvs timed out\n"));
					cvs_process_stop(proc);
				}
			} // end while
/*
#if TARGET_RT_MAC_MACHO
		app->SetCvsRunning(false);
		app->SetCvsProcess(NULL);
#endif
*/
		}
	}

	return sLastExitCode;
}

/*!
	Stop running CVS command
*/
void stopCVS()
{
#ifdef WIN32
	CWincvsApp* app = (CWincvsApp*)AfxGetApp();
	app->SetCvsStopping(true);

	cvs_process_stop(app->GetCvsProcess());
	cvs_out(_i18n("***** CVS stopped on user request !!! *****\n\n"));
#endif
}

/*!
	Test whether specified cvs binary supports cvsgui protocol
	\param cvsfile Cvs binary file
	\param argc Arguments count
	\param argv	Arguments
	\return	The process return error code
*/
int testCVS(const char* cvsfile, int argc, char* const* argv)
{
	return launchCVS(TRUE, NULL, argc, argv, NULL, cvsfile);
}
/*!
	Launch cvs command
	\param path Cvs binary file
	\param argc Arguments count
	\param argv	Arguments
	\param console Console
	\return The process return error code
*/
int launchCVS(const char* path, int argc, char* const* argv, CCvsConsole* console /*= 0L*/)
{
	return launchCVS(FALSE, path, argc, argv, console, NULL);
}

/*!
	Launch cvs command and send a stream of the cvs output into a file
	\param dir Directory
	\param args	Cvs arguments
	\param tmpFile File name
	\param prefix File prefix
	\param extension File extension
	\return The output file handle on success, NULL on error
*/
FILE* launchCVS(const char* dir, const CvsArgs& args, CStr& tmpFile, 
				const char* prefix /*= 0L*/, const char* extension /*= 0L*/)
{
	if( !MakeTmpFile(tmpFile, prefix, extension) )
		return 0L;

	FILE* res = fopen(tmpFile, gCvsPrefs.UnixLF() ? "wb+" : "w+");
	if( res == 0L )
	{
		cvs_err(_i18n("Impossible to open the temp file '%s' for write (error %d)"),
			(const char*)tmpFile, errno);

		return 0L;
	}

	CStreamConsole console(res);
	
	launchCVS(dir, args.Argc(), args.Argv(), &console);
	fseek(res, 0L, SEEK_SET);

	return res;
}

/*!
	Launch cvs command and put the cvs output in a file
	\param dir	Directory
	\param args	Cvs arguments
	\param prefix File prefix
	\param extension File extension
	\return The file name on success, NULL on error
	\note The temp file is automatically removed when quitting
*/
const char* launchCVS(const char* dir, const CvsArgs& args, 
					  const char* prefix /*= 0L*/, const char* extension /*= 0L*/)
{
	CStr filename;
	FILE* file = launchCVS(dir, args, filename, prefix, extension);
	if( file == 0L )
		return 0L;

	fclose(file);

	return sTmpList.push_back(filename);
}

#ifdef WIN32
void WaitForCvs(void)
{
	// pump messages manually
	CWincvsApp* app = (CWincvsApp*)AfxGetApp();

	while( TRUE )
	{
		if( app->GetCvsProcess() == 0L )
			return;

		if( !cvs_process_is_active(app->GetCvsProcess()) )
			return;

		Sleep(100);
	}
}
#endif

#if TARGET_RT_MAC_MACHO
void WaitForCvs(void)
{
	while( cvs_process_is_active(CMacCvsApp::gApp->GetCvsProcess()) )
	{
		UInt32 ticker = ::TickCount();
		
		while( cvs_process_give_time() )
		{
			if( (::TickCount() - ticker) >= 60 )
				break;
		}

		glue_eventdispatcher(0L);
	}

	CMacCvsApp::gApp->SetCvsRunning(false);
}
#endif

//////////////////////////////////////////////////////////////////////////
// 	CCvsStreamConsole

CCvsStreamConsole::CCvsStreamConsole(FILE* fileOut /*= 0L*/, bool autoClose /*= true*/)
	: m_fileOut(fileOut), m_autoClose(autoClose), m_modeSet(false)
{
}

CCvsStreamConsole::~CCvsStreamConsole()
{
	if( m_autoClose && m_fileOut )
	{
		fclose(m_fileOut);
	}
}

/*!
	Set the output file mode
	\param text true to set the text mode, false to set the binary mode
	\return true on success, false otherwise
	\note Typically the default mode is text
*/
void CCvsStreamConsole::SetMode(bool binary)
{
#ifdef WIN32
	if( binary )
	{
		setmode(fileno(m_fileOut), _O_BINARY);
	}
	else
	{
		setmode(fileno(m_fileOut), gCvsPrefs.UnixLF() ? _O_BINARY : _O_TEXT);
	}
#endif

	m_modeSet = true;
}

/*!
	Check whether the file translation mode was set already
	\return true if the file translation mode was set already, false otherwise
	\note Should be used before call to setmode to prevent unnecessary mode change
*/
bool CCvsStreamConsole::IsModeSet() const
{
	return m_modeSet;
}

/*!
	Open the output file
	\param fileName File name to be opened
	\return true on success, false otherwise
	\note File is opened as text or binary depending on the line feed preference settings
*/
bool CCvsStreamConsole::Open(const char* fileName)
{
	m_fileOut = fopen(fileName, gCvsPrefs.UnixLF() ? "wb+" : "w+");

	return 0L != m_fileOut;
}

long CCvsStreamConsole::cvs_out(char* txt, long len)
{
	if( !IsModeSet() )
	{
		SetMode(len == 0);
	}

	fwrite(txt, sizeof(char), len, m_fileOut);
	return len;
}

/// Call from override at the entry
long CCvsStreamConsole::cvs_err(char* txt, long len)
{
	cvs_errstr(txt, len);
	return len;
}


//////////////////////////////////////////////////////////////////////////
// CStreamConsole

CStreamConsole::CStreamConsole(FILE* fileOut) 
	: CCvsStreamConsole(fileOut, false)
{
}


//////////////////////////////////////////////////////////////////////////
// CAuthenSetUp

CAuthenSetUp::CAuthenSetUp()
{
	AuthenModel* curModel = AuthenModel::GetInstance(gAuthen.kind());
	if( curModel != 0L )
		curModel->OnSetupEnv();
}

CAuthenSetUp::~CAuthenSetUp()
{
	AuthenModel* curModel = AuthenModel::GetInstance(gAuthen.kind());
	if( curModel != 0L )
		curModel->OnRestoreEnv();
}

//////////////////////////////////////////////////////////////////////////
// CTmpFiles

CTmpFiles::CTmpFiles()
{
}

CTmpFiles::~CTmpFiles()
{
	std::vector<CStr>::const_iterator i;
	for(i = m_allfiles.begin(); i != m_allfiles.end(); ++i)
	{
		const char* file = *i;
		unlink((char*)file);
	}
}

inline const char* CTmpFiles::push_back(const char* file)
{
	m_allfiles.push_back(file);

	return m_allfiles[m_allfiles.size() - 1];
}
