/*
** 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 : Jerzy Kaczorowski <kaczoroj@hotmail.com> --- January 2004
 */

/*
 * LaunchHandlers.cpp : Launch helper functions
 */

#include "stdafx.h"

#ifdef WIN32
#	include "wincvs.h"
#endif /* !WIN32 */

#include "LaunchHandlers.h"
#include "CvsPrefs.h"
#include "AppConsole.h"
#include "CvsCommands.h"
#include "cvsgui_i18n.h"
#include "CvsArgs.h"

#if TARGET_RT_MAC_MACHO
#include "MacCvsApp.h"
#include "CBrowserPopup.h"
#include "GUSIFileSpec.h"
#include "MacCvsConstant.h"
#endif

#ifdef qUnix
#include "UCvsApp.h"
#endif

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

#ifdef WIN32
/// Generic check of shell action success
#define SHELLACTION_SUCCEEDED(shellActionResult) (shellActionResult >= 32)

/// Shell Action type enum
typedef enum{
	kShellActionDefault,	/*!< Execute OS default action for object */
	kShellActionOpen,		/*!< Open object in OS-associated handler application (or execute object itself if it is executable) */
	kShellActionEdit,		/*!< Open object in OS-associated editor application */
	kShellActionOpenAs,		/*!< Open OS handler selection dialog and open object with selected application */
	kShellActionExplore,	/*!< Open folder object in OS file system browser */
	kShellActionProperties	/*!< Open OS file properties dialog */
}kShellActionType;

/*!
	Get the launch type name
	\param launchType Launch type
	\param shortName true to retrieve the short name, false to get the long name
	\return The launch type name
*/
const char* GetLaunchTypeName(const kLaunchType launchType, const bool shortName /*= true*/)
{
	const char* res = "";

	switch( launchType )
	{
	case kLaunchEdit:
		res = shortName ? _i18n("Edit") : _i18n("Edit with the associated editor");
		break;
	case kLaunchDefaultEdit:
		res = shortName ? _i18n("Edit with editor") : _i18n("Edit with the default editor");
		break;
	case kLaunchOpen:
		res = shortName ? _i18n("Open") : _i18n("Open with the associated viewer");
		break;
	case kLaunchOpenAs:
		res = shortName ? _i18n("Open with...") : _i18n("Choose an application to open with");
		break;
	case kLaunchDefault:
		res = shortName ? _i18n("Default") : _i18n("Execute default associated shell action");
		break;
	default:
		break;
	}

	return res;
}

/*!
	Launch a file, folder or program via the OS' high-level API
	\param actionType Manner in which the given object is to be launched
	\param filePath File or folder name to launch (may include full path and arguments)
	\param args Arguments to pass to the file in case it is an application
	\param workdir Working directory to launch the file from
	\return Returns an OS-specific exit code - use SHELLACTION_SUCCEEDED to test for success
*/
static UINT ShellAction(const kShellActionType actionType, const char* filePath, const char* args  = 0L, const char* workdir = 0L)
{
	char* verb;

	switch( actionType )
	{
	case kShellActionDefault:
		verb = NULL;
		break;
	case kShellActionOpen:
		verb = "open";
		break;
	case kShellActionEdit:
		verb = "edit";
		break;
	case kShellActionOpenAs:
		verb = "openas";
		break;
	case kShellActionExplore:
		verb = "explore";
		break;
	case kShellActionProperties:
		verb = "properties";
		break;
	}
	
	HWND hParentWnd = AfxGetMainWnd()->GetSafeHwnd();
	
	SHELLEXECUTEINFO seInfo = { 0 };
	seInfo.cbSize = sizeof(SHELLEXECUTEINFO);
	seInfo.hwnd = hParentWnd;
	seInfo.lpVerb = verb;
	seInfo.lpFile = filePath;
	seInfo.lpParameters = args;
	seInfo.lpDirectory = workdir;
	seInfo.nShow = SW_SHOWDEFAULT;
	
	if( actionType != kShellActionOpenAs )
	{
		seInfo.fMask |= SEE_MASK_FLAG_NO_UI;
	}
	
	if( actionType == kShellActionProperties )
	{
		seInfo.fMask |= SEE_MASK_INVOKEIDLIST;
	}
	
	ShellExecuteEx(&seInfo);

	return (UINT)seInfo.hInstApp;
}
#endif

/*!
	Launch a diff of two files
	\param f1 First file name
	\param f2 Second file name
	\param diffTool diff tool to launch (optional)
	\param unifiedDiff if \a true then use unified diff if possible
	\param noWhiteSpace if \a true then ignore extra white space if possible
	\param noBlankLines if \a true then ignore blank lines if possible
	\param ignoreCase if \a true then ignore case differences if possible
	\return true on success, false otherwise
*/
bool LaunchDiff(const char* f1, const char* f2, 
				const char* diffTool /*= 0L*/, bool unifiedDiff /*= false*/, bool noWhiteSpace /*= false*/, bool noBlankLines /*= false*/, bool ignoreCase /*= false*/)
{
  if( diffTool == 0L  ||  diffTool[0] == 0 ) diffTool = gCvsPrefs.ExtDiff();
	if( diffTool == 0L  ||  diffTool[0] == 0 )
	{
		cvs_err(_i18n("The external diff program is not defined.\n"));
		cvs_err(_i18n("You need to set it in the preferences dialog.\n"));
		return false;
	}

#ifdef WIN32
	CStr args;
	CStr program, file1, file2;

	if( strchr(diffTool, ' ') != 0L )
	{
		program << '\"' << diffTool << '\"';
	}
	else
	{
		program = diffTool;
	}

	if( strchr(f1, ' ') != 0L )
	{
		file1 << '\"' << f1 << '\"';
	}
	else
	{
		file1 = f1;
	}

	if( strchr(f2, ' ') != 0L )
	{
		file2 << '\"' << f2 << '\"';
	}
	else
	{
		file2 = f2;
	}

	args << file1.c_str() << " " << file2.c_str();

	UINT shellRes = ShellAction(kShellActionDefault, diffTool, args.c_str());
	if( !SHELLACTION_SUCCEEDED(shellRes) )
	{
		// ExtDiff app was not found. Let's try to execute it from
		// the application path. However, this is done only when ExtDiff
		// value doesn't contain absolute path ("c:\subdir\" or "\subdir\").
		if( strchr(diffTool, ':') == 0L && (diffTool[0]) != '\\' )
		{
			CStr appPath;

			::GetAppPath(appPath);
			if( !appPath.endsWith(kPathDelimiter) )
				appPath << kPathDelimiter;

			appPath << diffTool;
		
			shellRes = ShellAction(kShellActionDefault, appPath, args);
		}
	}

	if( !SHELLACTION_SUCCEEDED(shellRes) ) 
	{
		LPVOID lpMsgBuf;
		FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
			NULL,
			GetLastError(),
			MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
			(LPTSTR)&lpMsgBuf,
			0,
			NULL);

		cvs_err(_i18n("Unable to open '%s' (%s), using console instead\n"), diffTool, (LPCSTR)lpMsgBuf);
		// Free the buffer.
		LocalFree( lpMsgBuf );
		return false;
	}

	return true;
#elif qUnix
	CvsArgs args(false);
	args.add(diffTool);
	args.add(f1);
	args.add(f2);
	UCvsApp::gApp->Execute(args.Argc(), args.Argv());
	return true;
#elif TARGET_RT_MAC_MACHO
	CvsArgs args(false);
	args.add(diffTool);
	if ( noWhiteSpace ) {
	  if ( strcmp(diffTool, kDiffTool_CodeWarrior) == 0  ||  strcmp(diffTool, kDiffTool_BBEdit) == 0 ) args.add("-wb");
	}
	if ( ignoreCase ) {
	  if ( strcmp(diffTool, kDiffTool_CodeWarrior) == 0  ||  strcmp(diffTool, kDiffTool_BBEdit) == 0 ) args.add("-i");
	}
	args.add(f1);
	args.add(f2);
	CMacCvsApp::gApp->Execute(args.Argc(), args.Argv());
	return true;
#endif
}

/*!
	Launch a handler for file
	\param launchType Type of the requested handler type
	\param filePath Full path to the file
	\return true on success, false otherwise
*/
bool LaunchHandler(const kLaunchType launchType, const char* filePath)
{
	bool res = false;

#ifdef WIN32
	CStr folder, file;
	SplitPath(filePath, folder, file);

	if( launchType == kLaunchOpen )
	{
		// Explicitly attempt "open"
		res = SHELLACTION_SUCCEEDED(ShellAction(kShellActionOpen, filePath, NULL, folder));
		if( !res )
		{
			// Attempt OS default action for object
			res = SHELLACTION_SUCCEEDED(ShellAction(kShellActionDefault, filePath, NULL, folder));
		}
	}
	
	if( !res && (launchType == kLaunchOpen || launchType == kLaunchOpenAs) )
	{
		// Show OS handler selection dialog
		res = SHELLACTION_SUCCEEDED(ShellAction(kShellActionOpenAs, filePath, NULL, folder));
		if( !res )
		{
			cvs_err(_i18n("Unable to open '%s' (error %d), using editor instead\n"), (char*)filePath, GetLastError());
		}
	}
	
	if( !res && launchType != kLaunchDefaultEdit && launchType != kLaunchDefault )
	{
		// Attempt "edit" action
		res = SHELLACTION_SUCCEEDED(ShellAction(kShellActionEdit, filePath, NULL, folder));
	}

	if( !res && launchType == kLaunchDefault )
	{
		res = SHELLACTION_SUCCEEDED(ShellAction(kShellActionDefault, filePath, NULL, folder));
	}
#endif
	
	if( !res )
	{
		// If all else fails, open in default editor
		res = LaunchDefaultEditor(filePath);
	}
	
	return res;
}

/*!
	Launch a viewer for file
	\param filePath Full path to the file
	\param useOpenAs true to always show "Open with" dialog, false to only show it if no valid association exists
	\return true on success, false otherwise
*/
bool LaunchViewer(const char* filePath, bool useOpenAs /* = false */)
{
	bool res = false;
	
#ifdef WIN32
	bool needOpenAs = false;
	
	CStr folder, file;
	SplitPath(filePath, folder, file);
	
	if( !useOpenAs )
	{
		UINT shellRes = ShellAction(kShellActionOpen, filePath, NULL, folder);
		if( !SHELLACTION_SUCCEEDED(shellRes))
		{
			// If it failed because of no valid association and file has an extension then try the "Open with..." dialog instead
			needOpenAs = (file.rfind('.') > 0 && 
				(SE_ERR_NOASSOC == shellRes || SE_ERR_ASSOCINCOMPLETE == shellRes));
		}
		else
		{
			res = true;
		}
	}
	
	if( !res && (useOpenAs || needOpenAs) )
	{
		res = SHELLACTION_SUCCEEDED(ShellAction(kShellActionOpenAs, filePath, NULL, folder));
	}
	
	if( !res )
	{
		cvs_err(_i18n("Unable to open '%s' (error %d), using default editor instead\n"), (char*)filePath, GetLastError());
		res = LaunchEditor(filePath);
	}

#endif /* WIN32 */
	
#ifdef qMacCvsPP 
  // fall thru to calling LaunchEditor below...
#endif /* qMacCvsPP */
	
#if qUnix
	  res = true;
#endif // qUnix

	if( !res )
	{
		res = LaunchEditor(filePath);
	}

	return res;
}

/*!
	Launch an editor for file
	\param filePath Full path to the file
	\param useDefault true to use OS setup editor, false to use default editor
	\return true on success, false otherwise
*/
bool LaunchEditor(const char* filePath, bool useDefault /*= false*/)
{
	bool res = false;
	
#ifdef WIN32
	if( !useDefault )
	{
		CStr folder, file;
		SplitPath(filePath, folder, file);
		
		res = SHELLACTION_SUCCEEDED(ShellAction(kShellActionEdit, filePath, NULL, folder));
	}
	
#endif /* WIN32 */
	
#ifdef qMacCvsPP
	if ( !useDefault )
	{
		GUSIFileSpec  f(filePath);
		OSStatus      err;
		err = LSOpenFSRef(f, NULL);
		FSRef         application;
		
		err = LSGetApplicationForItem(f, kLSRolesAll, &application, NULL);
		if ( err != noErr )
		{
			// no luck finding a default binding -> try as text file
			err = LSGetApplicationForInfo('TEXT', kLSUnknownCreator, CFSTR("txt"), kLSRolesAll, &application, NULL);
		}
		if ( err == noErr )
		{
			LSLaunchFSRefSpec   launchSpec = { &application, 1, f, NULL, kLSLaunchDefaults|kLSLaunchDontAddToRecents, NULL };
			err = LSOpenFromRefSpec(&launchSpec, NULL);
		}
		if ( err != noErr )
		{
			cvs_err(_i18n("Unable to open '%s' (error %ld), using default editor instead\n"), (char*)filePath, err);
		}
		else res = true;
	}
#endif /* qMacCvsPP */
	
#if qUnix
#endif // qUnix
	
	if( !res )
	{
		res = LaunchDefaultEditor(filePath);
	}
	
	return res;
}

/*!
	Launch the default editor for a file
	\param filePath File name
	\param par Return data parameter to return the info about running editor process
	\return true on success, false otherwise
*/
bool LaunchDefaultEditor(const char* filePath, void* par /* = 0L */)
{
	bool res = false;

#ifndef qMacCvsPP
	if( gCvsPrefs.Viewer() == 0L )
	{
		cvs_err(_i18n("You need to define a default editor in the preferences\n"));
		return false;
	}
	
	CStr program, file;

	if( strchr(gCvsPrefs.Viewer(), ' ') != 0L )
	{
		program << '\"';
		program << gCvsPrefs.Viewer();
		program << '\"';
	}
	else
		program = gCvsPrefs.Viewer();

	if( strchr(filePath, ' ') != 0L )
	{
		file << '\"';
		file << filePath;
		file << '\"';
	}
	else
		file = filePath;

	CStr command;
	command << program;
	command << " ";
	command << file;
#endif

#ifdef WIN32
	STARTUPINFO sStartupInfo = { 0 };
	PROCESS_INFORMATION sProcessInformation = { 0 };

	// Execute the command with a call to the CreateProcess API call.
	sStartupInfo.cb = sizeof(sStartupInfo);
	sStartupInfo.dwFlags = STARTF_USESHOWWINDOW;
	sStartupInfo.wShowWindow = SW_SHOWNORMAL;
	
	if( !CreateProcess(NULL,
		(char*)command.c_str(),
		NULL,
		NULL,
		FALSE,
		0,
		NULL,
		NULL, 
		&sStartupInfo,
		&sProcessInformation) )
	{
		sProcessInformation.hProcess = INVALID_HANDLE_VALUE;
		sProcessInformation.hThread = INVALID_HANDLE_VALUE;
	}
	
	if( sProcessInformation.hProcess != INVALID_HANDLE_VALUE )
	{
		res = true;
	}
	
	if( par )
	{
		*((PROCESS_INFORMATION*)par) = sProcessInformation;
	}
	else if( res )
	{
		CloseHandle(sProcessInformation.hProcess);
		CloseHandle(sProcessInformation.hThread);
	}

#endif /* WIN32 */

#ifdef qMacCvsPP
	GUSIFileSpec  f(filePath);
	OSStatus      err(-1);
	FSRef         application;
	bool          gotViewer(false);
	
	if( gCvsPrefs.Viewer() != NULL )
	{
		// viewer contains creator and/or bundle identifier
		if ( strlen(gCvsPrefs.Viewer()) >= 4 )
		{
			OSType      appCreator;
			CFStringRef bundleIdentifier(NULL);
			
			memcpy(&appCreator, gCvsPrefs.Viewer(), 4);
			if ( strlen(gCvsPrefs.Viewer()) > 4  &&  gCvsPrefs.Viewer()[4] == ':' )
				bundleIdentifier = CFStringCreateWithCString(NULL, &gCvsPrefs.Viewer()[5], kCFStringEncodingUTF8);
			err = LSFindApplicationForInfo(appCreator == '\?\?\?\?' ? kLSUnknownCreator : appCreator, bundleIdentifier, NULL, &application, NULL);
			if ( err != noErr )
			{
				cvs_err(_i18n("Unable to find your default viewer '%s' (error %ld), will use default text editor.\n"), gCvsPrefs.Viewer(), err);
			}
		}
	}
	if ( err != noErr )
	{
		// use the default text file viewer
		err = LSGetApplicationForInfo('TEXT', kLSUnknownCreator, CFSTR("txt"), kLSRolesAll, &application, NULL);
	}
	
	if ( err == noErr )
	{
		LSLaunchFSRefSpec   launchSpec = { &application, 1, f, NULL, kLSLaunchDefaults|kLSLaunchDontAddToRecents, NULL };
		err = LSOpenFromRefSpec(&launchSpec, NULL);
	}
	if ( err != noErr )
	{
		cvs_err(_i18n("Unable to open '%s' (error %ld) with default editor '%s'.\n"), (char*)filePath, err, gCvsPrefs.Viewer() ? gCvsPrefs.Viewer() : "<default text editor>");
	}
	else 
		res = true;
#endif /* qMacCvsPP */
	
#if qUnix
	res = true;
#endif // qUnix

	return res;
}

/*!
	Helper function to explore given path
	\param path Path to explore
*/
void LaunchExplore(const char* path)
{
#ifdef WIN32
	if( !SHELLACTION_SUCCEEDED(ShellAction(kShellActionExplore, path)) )
	{
		cvs_err("Unable to explore '%s' (error %d)\n", (char*)path, GetLastError());
	}
#endif
#ifdef qMacCvsPP
  GUSIFileSpec    fileSpec(path);
  CBrowserPopup::SendFinderOpenAE(fileSpec, true);
#endif
#ifdef qUnix
	CvsArgs args(false);
	args.add(gCvsPrefs.Browser());
	args.add(path);
	UCvsApp::gApp->Execute(args.Argc(), args.Argv());
#endif
}

/*!
	Helper function to open given path in a new instance
	\param path Path to open
*/
void LaunchNewInst(const char* path)
{
	UStr appPath;

#ifdef WIN32
	::GetAppModule(appPath);

	if( !SHELLACTION_SUCCEEDED(ShellAction(kShellActionDefault, appPath, path)) )
	{
		cvs_err("Unable to open new instance for '%s' (error %d)\n", (char*)path, GetLastError());
	}
#endif
#ifdef qMacCvsPP
#endif
#ifdef qUnix
#endif
}

/*!
	Helper function to show the properties of a given path
	\param path Path to show the properties
*/
void LaunchProperties(const char* path)
{
#ifdef WIN32
	if( !SHELLACTION_SUCCEEDED(ShellAction(kShellActionProperties, path)) )
	{
		cvs_err("Unable to show the properties '%s' (error %d)\n", (char*)path, GetLastError());
	}
#endif
#ifdef qMacCvsPP
#endif
#ifdef qUnix
#endif
}
