/*
** 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
 */

#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 "wincvsView.h"
#	include "PromptFiles.h"
#	include "BrowseFileView.h"
#	include <process.h>
#	include <direct.h>

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

#if !TARGET_RT_MAC_CFM
#	include "cvsgui_process.h"
#endif

#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"

/// 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

#if !TARGET_RT_MAC_CFM
static CvsProcessCallbacks sCallTable = {
	glue_consoleout,
	glue_consoleerr,
	glue_getenv,
	glue_exit
};
#endif

/*!
	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("\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);
	
#ifdef TARGET_OS_MAC
	// apple event output
	long newlen;
	if( (newlen = ae_consoleout(txt, len)) != -1 )
		return newlen;
#endif

	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);
	
#ifdef TARGET_OS_MAC
	// apple event output
	long newlen;
	if( (newlen = ae_consoleout(txt, len)) != -1 )
		return newlen;
#endif

	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)
{
#ifdef TARGET_OS_MAC
	// apple event input
	long newlen;
	if( (newlen = ae_consolein(txt, len)) != -1 )
		return newlen;
#endif /* !TARGET_OS_MAC */

	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 this means the CVSROOT is local
#ifdef TARGET_OS_MAC
			cvs_err("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("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[512];
			getcwd(oldpath, 511);
			
			const char* chome = BrowserGetDirectory("Select your home directory :");
			if( chome == 0L )
			{
				chdir(oldpath);
				return 0L;
			}

			gCvsPrefs.SetHome(chome);
			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
#if TARGET_RT_MAC_CFM
	else if( strcmp(name, "HOME") == 0 )
	{
		UFSSpec theFolder;
		static UStr thePath;
		
		if( MacGetPrefsFolder(theFolder, thePath) != noErr )
		{
			cvs_err("Unable to locate the preferences folder !\n");
			return 0L;
		}
		
		// We do not use any more the "@@@" trick. Instead
		// we let cvs convert it to a Unix path.
		//strcpy(thePath, "@@@");
		return thePath;
	}
	else if( strcmp(name, "CVSUSEUNIXLF") == 0 )
	{
		return gCvsPrefs.UnixLF() ? (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;
}

/*!
	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)
{
	CAuthenSetUp setupEnv;
	CvsProcessStartupInfo startupInfo;

	startupInfo.test = true;
	startupInfo.hasTty = gCvsPrefs.CvsConsole();

#if TARGET_RT_MAC_CFM
	return -1;
#elif defined(WIN32) || qUnix || TARGET_RT_MAC_MACHO
	CStr cvscmd(cvsfile);

	sLastExitCode = 1;

#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("Error : cvs is already running !\n");
		return errInternal;
	}
	
	app->SetCvsStopping(false);

	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("Unable to find the cvs process in : (error %d)\n", errno);
		cvs_err("1) %s\n", (const char*)cvscmd);
		return -1;
	}

	CvsProcess* proc = cvs_process_run(cvscmd, argc - 1, (char**)argv + 1, &sCallTable, &startupInfo);

	if( proc == 0L )
	{
		cvs_err("Unable to initialize the cvs process (error %d)\n", errno);
		cvs_err("The cvs used is : %s\n", (const char*)cvscmd);
	}
	else 
	{
		app->SetCvsRunning(true);
		app->SetCvsProcess(proc);
#if TARGET_RT_MAC_MACHO
		UInt32 ticker = ::TickCount();
#else
		app->PeekPumpAndIdle(true); 
#endif

		time_t start = time(NULL);
		
		while( (time(NULL) - start ) < 20) 
		{
#if TARGET_RT_MAC_MACHO
			if( (::TickCount () - ticker) > 40 )
			{
				glue_eventdispatcher(0L);
				ticker = ::TickCount ();
			}
#else
			app->PeekPumpAndIdle(false); 
#endif
			cvs_process_give_time();
				
			if( !cvs_process_is_active(proc) )
				break;
		
			if( (time(NULL) - start) > 10 ) 
			{
				cvs_err("testing cvs timed out");
				cvs_process_stop(proc);
			}
		}
	}

	return sLastExitCode;
#endif
}

/*!
	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*/)
{
	CAuthenSetUp setupEnv;
	CvsProcessStartupInfo startupInfo;

	startupInfo.hasTty = gCvsPrefs.CvsConsole();
	startupInfo.currentDirectory = path;
	
	gConsole = console;

#if TARGET_RT_MAC_CFM
	CompatConnectID connID;
	
	if( !loadCVS(connID) )
		return errInternal;
	
	int exitc = 0;

	// call the main
	if( !CompatCallLibrary3(connID, exitc, uppMainProcInfo, "dllglue_main",
		dllglue_main_func, path, argc, (char**)argv) )
	{
		cvs_err("Error : Cannot find symbol ""dllglue_main""\n");
		exitc = errInternal;
	}
	
	unloadCVS(connID, exitc);
	
	return exitc;

#elif defined(WIN32) || defined(qUnix) || TARGET_RT_MAC_MACHO
	CStr cvscmd;
	
	sLastExitCode = 0;

#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("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);
	
	gCvsPrefs.WhichCvs(cvscmd, gCvsPrefs.UseAltCvs());
	
	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("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("Unable to initialize the cvs process (error %d)\n", errno);
		cvs_err("The cvs used is : %s\n", (const char*)cvscmd);
	}
	else 
	{
		app->SetCvsRunning(true);
		app->SetCvsProcess(proc);
#if defined(WIN32) || defined(qUnix)
		app->PeekPumpAndIdle(true); 
#endif
#ifndef qUnix
                if( console != 0L )
#endif
		{
			while( TRUE )
			{
#if TARGET_RT_MAC_MACHO
				UInt32 ticker = ::TickCount();
#elif defined(WIN32) || defined(qUnix)
				app->PeekPumpAndIdle(false); 
#endif

#if TARGET_RT_MAC_MACHO
				if( (::TickCount () - ticker) > 40 )
				{
					glue_eventdispatcher(0L);
					ticker = ::TickCount ();
				}
#endif
				cvs_process_give_time();
			
				if( !cvs_process_is_active(proc) )
					break;
			} // end while
		}
	}

	return sLastExitCode;

#endif
}

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

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

/*!
	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, "w+");
	if( res == 0L )
	{
		cvs_err("Impossible to open for write the temp file '%s' (error %d)",
			(const char*)tmpFile, errno);

		return 0L;
	}

	CStreamConsole myconsole(res);
	
	launchCVS(dir, args.Argc(), args.Argv(), &myconsole);
	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

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

CStreamConsole::CStreamConsole(FILE* out) 
	: m_fOut(out)
{
}

long CStreamConsole::cvs_out(char* txt, long len)
{
	fwrite(txt, sizeof(char), len, m_fOut);
	return len;
}

long CStreamConsole::cvs_err(char* txt, long len)
{
	cvs_errstr(txt, len);
	return len;
}

//////////////////////////////////////////////////////////////////////////
// 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];
}
