/*
 * Copyright (C) 2005-2007 Alfresco Software Limited.
 *
 * 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 2
 * of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

 * As a special exception to the terms and conditions of version 2.0 of 
 * the GPL, you may redistribute this Program in connection with Free/Libre 
 * and Open Source Software ("FLOSS") applications as described in Alfresco's 
 * FLOSS exception.  You should have recieved a copy of the text describing 
 * the FLOSS exception, and it is also available here: 
 * http://www.alfresco.com/legal/licensing"
 */

#include "stdafx.h"
#include "CAlfrescoApp.h"
#include "CAlfrescoAppDlg.h"

#include <stdlib.h>

#include "util\String.h"
#include "util\DataBuffer.h"
#include "util\FileName.h"
#include "util\Integer.h"
#include "util\Debug.h"

#include <shellapi.h>

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

using namespace std;
using namespace Alfresco;

// CCAlfrescoAppApp

BEGIN_MESSAGE_MAP(CAlfrescoApp, CWinApp)
	ON_COMMAND(ID_HELP, CWinApp::OnHelp)
END_MESSAGE_MAP()

// CCAlfrescoApp construction

CAlfrescoApp::CAlfrescoApp()
{
	// TODO: add construction code here,
	// Place all significant initialization in InitInstance
}


// The one and only CCAlfrescoAppApp object

CAlfrescoApp theApp;


// CCAlfrescoAppApp initialization

BOOL CAlfrescoApp::InitInstance()
{
	// InitCommonControls() is required on Windows XP if an application
	// manifest specifies use of ComCtl32.dll version 6 or later to enable
	// visual styles.  Otherwise, any window creation will fail.

	InitCommonControls();
	CWinApp::InitInstance();
	AfxEnableControlContainer();

	// Check if debug logging is enabled

	char dbgLogName[MAX_PATH];
	size_t dbgLogSize;

	if ( getenv_s( &dbgLogSize, dbgLogName, sizeof( dbgLogName), "ALFDEBUG") == 0) {

		// Enable debug output

		Debug::openLog( dbgLogName);

		// Log the application startup

		DBGOUT_TS << "---------- Desktop client app started ----------" << endl; 
	}

	// Get the application path

	String appPath = __wargv[0];

	int pos = appPath.lastIndexOf(PathSeperator);

	if ( pos < 0) {
		AfxMessageBox( L"Invalid application path", MB_OK | MB_ICONSTOP);
		DBGOUT_TS << "Error, bad application path, " << appPath << endl;
		return 1;
	}

	// Get the path to the folder containing the application

	String folderPath = appPath.substring(0, pos);
	String exeName    = appPath.substring(pos + 1);

	// Create a list of the command line arguments

	StringList argList;
	bool argSetWorkDir = false;

	for ( int i = 1; i < __argc; i++) {

		// Check if the argument is a path or switch

		String arg = __wargv[i];

		if ( arg.startsWith( "/")) {

			// Check for the set working directory switch

			if ( arg.equalsIgnoreCase( "/D")) {
				argSetWorkDir = true;

				// DEBUG

				DBGOUT_TS << "/D switch specified" << endl;
			}
			else {
				String msg = L"Invalid command line switch - ";
				msg.append( arg);
				AfxMessageBox( msg.data(), MB_OK | MB_ICONSTOP);
				DBGOUT_TS << "Error, " << msg << endl;
				return 2;
			}
		}
		else {

			// Add the path to the argument list

			argList.addString( arg);
		}
	}

	// Check if the working directory should be set to the path of the first document

	if ( argSetWorkDir == true) {

		// Check if there are any document paths

		if ( argList.numberOfStrings() == 0) {
			AfxMessageBox( L"Cannot set working directory, no document paths", MB_OK | MB_ICONSTOP);
			DBGOUT_TS << "Error, cannot set working directory, no document paths" << endl;
			return 3;
		}

		// Get the first document path and remove the file name

		String docPath = argList[0];
		pos = docPath.lastIndexOf( PathSeperator);

		if ( pos < 0) {
			AfxMessageBox( L"Invalid document path", MB_OK | MB_ICONSTOP);
			DBGOUT_TS << "Error, invalid document path, " << docPath << endl;
			return 4;
		}

		// Set the document path as the working directory folder

		folderPath = docPath.substring(0, pos);

		// DEBUG

		DBGOUT_TS << "Using document path as working directory, " << folderPath << endl;
	}

	// Create the Alfresco interface

	AlfrescoInterface alfresco(folderPath);

	if ( alfresco.isAlfrescoFolder()) {

		try {

			// DEBUG

			DBGOUT_TS << "Using folder " << folderPath << endl;

			// Get the action information

			AlfrescoActionInfo actionInfo = alfresco.getActionInformation(exeName);

			// DEBUG

			if ( HAS_DEBUG) {
				DBGOUT_TS << "Action " << actionInfo.getName() << endl;
				DBGOUT_TS << "  PreProcess: ";

				if ( actionInfo.hasPreProcessAction( PreConfirmAction))
					DBGOUT << "Confirm ";
				if ( actionInfo.hasPreProcessAction( PreCopyToTarget))
					DBGOUT << "CopyToTarget ";
				if ( actionInfo.hasPreProcessAction( PreLocalToWorkingCopy))
					DBGOUT << "LocalToWorkingCopy";
				DBGOUT << endl;
			}

			// Check if the action should be confirmed

			if ( actionInfo.hasPreProcessAction(PreConfirmAction)) {

				// Get the confirmation message

				String confirmMsg = actionInfo.getConfirmationMessage();
				if ( confirmMsg.length() == 0)
					confirmMsg = L"Run action ?";

				// DEBUG

				DBGOUT_TS << "Confirm action, message = " << confirmMsg << endl;

				// Display a confirmation dialog

				if ( AfxMessageBox( confirmMsg, MB_OKCANCEL | MB_ICONQUESTION) == IDCANCEL) {
					DBGOUT_TS << "User cancelled action" << endl;
					return FALSE;
				}
			}

			// Check if the action supports multiple paths, if not then call the action once for each supplied path

			if ( actionInfo.hasAttribute(AttrMultiplePaths)) {

				// Run the action

				runAction( alfresco, argList, actionInfo);
			}

			// Check if the action supports file/folder targets

			else if ( actionInfo.hasAttribute( AttrAnyFilesFolders) == true) {

				// Pass one path at a time to the action

				for ( size_t i = 0; i < argList.numberOfStrings(); i++) {

					// Create a path list with a single path

					StringList pathList;
					pathList.addString( argList[i]);

					// Run the action

					runAction( alfresco, pathList, actionInfo);
				}
			}

			// Action does not use targets, just run the action

			else if ( actionInfo.allowsNoParameters()) {

				// Run the action

				StringList emptyList;
				runAction( alfresco, emptyList, actionInfo);
			}
		}
		catch (Exception ex) {
			CString msg;
			msg.FormatMessage( L"Exception occurred\n\n%1", ex.getMessage().data());
			AfxMessageBox( msg, MB_OK | MB_ICONSTOP);
		}
	}
	else {
		AfxMessageBox( L"Not a valid Alfresco CIFS folder", MB_OK | MB_ICONSTOP);
		DBGOUT_TS << "Error, not a valid Alfresco CIFS folder, " << folderPath << endl;
		return 1;
	}

	//	Exit the application

	return FALSE;
}

/**
 * Process the command line arguments and build the parameter list for the desktop action
 *
 * @param alfresco AlfrescoInterface&
 * @param paths StringList&
 * @param actionInfo AlfrescoActionInfo&
 * @param params DesktopParams&
 * @return bool
 */
bool CAlfrescoApp::buildDesktopParameters( AlfrescoInterface& alfresco, StringList& paths, AlfrescoActionInfo& actionInfo,
										    DesktopParams& params) {

	// If there are no paths then just return a success

    if ( paths.numberOfStrings() == 0)
	    return true;

	// Process the list of files and either check in the file if it is a working copy or check out
	// the file

	for ( unsigned int i = 0; i < paths.numberOfStrings(); i++) {

		// Get the current file name

		String curFile = paths.getStringAt( i);

		// DEBUG

		DBGOUT_TS << "Parameter: " << curFile << endl;

		// Check if the path is on an Alfresco mapped drive

		if ( alfresco.isMappedDrive() && curFile.startsWithIgnoreCase( alfresco.getDrivePath())) {

			// Convert the path to a UNC path

			String uncPath = alfresco.getRootPath();
			uncPath.append( curFile.substring(2));

			curFile = uncPath;
		}

		// Check if the path is to a file/folder, and whether it is a local path

		bool copyFile = false;
		DWORD attr = GetFileAttributes( curFile);

		if ( attr != INVALID_FILE_ATTRIBUTES) {
			
			// Check if the action supports the file/folder type

			bool isDir = (attr & FILE_ATTRIBUTE_DIRECTORY) != 0 ? true : false;

			if ( isDir && actionInfo.supportsFolders() == false) {
				AfxMessageBox(L"Action does not support folders", MB_OK | MB_ICONSTOP);
				DBGOUT_TS << "Error, action does not support folders" << endl;
				return false;
			}
			else if ( actionInfo.supportsFiles() == false) {
				AfxMessageBox(L"Action does not support files", MB_OK | MB_ICONSTOP);
				DBGOUT_TS << "Error, action does not support files" << endl;
				return false;
			}

			// Get the file name from the path

			StringList nameParts = FileName::splitPath( curFile);
			String curName = nameParts.getStringAt( 1);

			// If the path is to a file that is not on the Alfresco share the file will need to be copied,
			// after checking the status of a matching file in the Alfresco folder

			if ( curFile.length() >= 3 && curFile.substring(1,3).equals( L":\\")) {

				// Check if the action supports local files

				if ( isDir == false && actionInfo.hasAttribute(AttrClientFiles) == false) {
					AfxMessageBox(L"Action does not support local files", MB_OK | MB_ICONSTOP);
					DBGOUT_TS << "Error, action does not support local files" << endl;
					return false;
				}
				else if ( isDir == true && actionInfo.hasAttribute(AttrClientFolders) == false) {
					AfxMessageBox(L"Action does not support local folders", MB_OK | MB_ICONSTOP);
					DBGOUT_TS << "Error, action does not support local folders" << endl;
					return false;
				}

				// Check if there is an existing file in the Alfresco with the same name, check if the file is locked
				
				PTR_AlfrescoFileInfo fInfo = alfresco.getFileInformation( curName);
				if ( fInfo.get() != NULL) {

					// There is an existing file in the Alfresco folder with the same name, check if it is locked

					if ( fInfo->getLockType() != LockNone) {
						AfxMessageBox( L"Cannot copy file to Alfresco folder, destination file is locked", MB_OK | MB_ICONEXCLAMATION);
						DBGOUT_TS << "Error, cannot copy to Alfresco folder, destination file is locked" << endl;
						return false;
					}
					else if ( actionInfo.hasPreProcessAction(PreLocalToWorkingCopy) == true && fInfo->isWorkingCopy() == false) {
						AfxMessageBox( L"Cannot copy to Alfresco folder, destination must overwrite a working copy", MB_OK | MB_ICONEXCLAMATION);
						DBGOUT_TS << "Error, cannot copy to Alfresco folder, destination must overwrite a working copy" << endl;
						return false;
					}
				}
				else if ( actionInfo.hasPreProcessAction(PreLocalToWorkingCopy) == true) {

					// Target folder does not contain a matching working copy of the local file

					CString msg;
					msg.FormatMessage( L"No matching working copy for %1", curName.data());
					AfxMessageBox( msg, MB_OK | MB_ICONEXCLAMATION);
					DBGOUT_TS << "Error, no matching working copy for " << curName << endl;
					return false;
				}

				// Copy the files/folders using the Windows shell

				bool copyAborted = false;

				if ( copyFilesUsingShell( curFile, alfresco.getUNCPath(), copyAborted) == false) {

					// Check if the copy failed or the user aborted the copy

					if ( copyAborted == false) {

						// File copy failed

						CString msg;
						msg.FormatMessage( isDir ? L"Failed to copy folder %1" : L"Failed to copy file %1", curFile.data());

						AfxMessageBox( msg, MB_OK | MB_ICONSTOP);
						DBGOUT_TS << "Error, copy failed for " << curName << endl;
						return false;
					}
					else {

						// User aborted the file copy

						CString msg;
						msg.FormatMessage( L"Copy aborted for %1", curFile.data());
						AfxMessageBox( msg, MB_OK | MB_ICONSTOP);
						DBGOUT_TS << "Error, copy aborted by user, " << curName << endl;
						return false;
					}
				}

				// DEBUG

				DBGOUT_TS << "Added target " << curName << endl;

				// Add a desktop target for the copied file

				params.addTarget( new DesktopTarget(isDir ? TargetCopiedFolder : TargetCopiedFile, curName));
			}
			else {

				//	Path is a UNC path, check if the file/folder is in the same folder as the action

				DesktopTarget* pTarget = NULL;

				if ( curFile.startsWith( alfresco.getUNCPath())) {

					// Path is in the same folder as the application, or in a sub-folder

					String relPath = curFile.substring( alfresco.getUNCPath().length() + 1);

					if ( relPath.indexOf( L"\\") == -1) {

						// Create a target using the file name only

						pTarget = new DesktopTarget( isDir ? TargetFolder : TargetFile, relPath);
					}
				}

				// If the target is not valid the file/folder is not in the same folder as the client-side application,
				// copy the files/folders to the target folder or use the root relative path to the file/folder

				if ( pTarget == NULL) {

					// Check if Alfresco files/folders should be copied to the target folder

					if ( actionInfo.hasPreProcessAction(PreCopyToTarget)) {

						// Copy the files/folders using the Windows shell

						bool copyAborted = false;

						if ( copyFilesUsingShell( curFile, alfresco.getUNCPath(), copyAborted) == false) {

							// Check if the copy failed or the user aborted the copy

							if ( copyAborted == false) {

								// File copy failed

								CString msg;
								msg.FormatMessage( isDir ? L"Failed to copy folder %1" : L"Failed to copy file %1", curFile.data());

								AfxMessageBox( msg, MB_OK | MB_ICONSTOP);
								DBGOUT_TS << "Error, copy failed for " << curName << endl;
								return false;
							}
							else {

								// User aborted the file copy

								CString msg;
								msg.FormatMessage( L"Copy aborted for %1", curFile.data());
								AfxMessageBox( msg, MB_OK | MB_ICONSTOP);
								DBGOUT_TS << "Error, copy aborted for " << curName << endl;
								return false;
							}
						}

						// Add a desktop target for the copied file

						pTarget= new DesktopTarget(isDir ? TargetCopiedFolder : TargetCopiedFile, curName);
					}
					else {

						// Get the root relative path to the file/folder

						String rootRelPath = curFile.substring(alfresco.getRootPath().length());
						pTarget = new DesktopTarget( isDir ? TargetFolder : TargetFile, rootRelPath);
					}
				}

				// DEBUG

				DBGOUT_TS << "Added target " << pTarget->getTarget() << endl;

				// Add the desktop target

				params.addTarget( pTarget);
			}
		}
	}

	// Return status

	return true;
}

/**
 * Copy a file/folder using the Windows shell
 *
 * @param fromFileFolder const String&
 * @param toFolder const String&
 * @param aborted bool&
 * @return bool
 */
bool CAlfrescoApp::copyFilesUsingShell(const String& fromFileFolder, const String& toFolder, bool& aborted) {

	// DEBUG

	DBGOUT_TS << "Copy from " << fromFileFolder << " to " << toFolder << endl;

	// Build the from/to paths, must be double null terminated

	wchar_t fromPath[MAX_PATH + 1];
	wchar_t toPath[MAX_PATH + 1];

	memset( fromPath, 0, sizeof( fromPath));
	memset( toPath, 0, sizeof( toPath));

	wcscpy( fromPath, fromFileFolder.data());
	wcscpy( toPath, toFolder.data());

	// Copy the local file to the Alfresco folder

	SHFILEOPSTRUCT fileOpStruct;
	memset( &fileOpStruct, 0, sizeof(SHFILEOPSTRUCT));

	fileOpStruct.hwnd  = HWND_DESKTOP;
	fileOpStruct.wFunc = FO_COPY;
	fileOpStruct.pFrom = fromPath;
	fileOpStruct.pTo   = toPath;
	fileOpStruct.fFlags= 0;
	fileOpStruct.fAnyOperationsAborted =false;

	// Copy the file to the Alfresco folder

	bool sts = false;

	if ( SHFileOperation( &fileOpStruct) == 0) {

		// File copy successful

		sts = true;
	}
	else if ( fileOpStruct.fAnyOperationsAborted) {

		// User aborted the file copy

		aborted = true;
	}

	// Return the copy status

	return sts;
}

/**
 * Run an action
 *
 * @param alfresco AlfrescoInterface&
 * @param pathList StringList&
 * @param actionInfo AlfrescoActionInfo&
 * @return bool
 */
bool CAlfrescoApp::runAction( AlfrescoInterface& alfresco, StringList& pathList, AlfrescoActionInfo& actionInfo) {

	// Build the desktop action parameter list, perform any file copying of local files

	bool sts = false;
	DesktopParams desktopParams;
	
	if ( buildDesktopParameters( alfresco, pathList, actionInfo, desktopParams)) {

		// Check if the action requires parameters

		if ( actionInfo.allowsNoParameters() == false && desktopParams.numberOfTargets() == 0) {
			AfxMessageBox( L"No parameters for action", MB_OK | MB_ICONEXCLAMATION);
			DBGOUT_TS << "Error, no parameters for action" << endl;
			return false;
		}

		// Run the desktop action

		DesktopResponse response = alfresco.runAction( actionInfo, desktopParams);

		// Check the response status

		if ( response.getStatus() != StsSuccess) {

			// Check if the status indicates a command line should be launched

			if ( response.getStatus() == StsCommandLine) {

				// DEBUG

				DBGOUT_TS << "Action returned command line, " << response.getStatusMessage() << endl;

				// Launch a process using the command line

				sts = doCommandLine( alfresco, response.getStatusMessage());
			}

			// Check if a web browser should be launched with a URL

			else if ( response.getStatus() == StsLaunchURL) {

				// DEBUG

				DBGOUT_TS << "Action returned URL, " << response.getStatusMessage() << endl;

				// Use the Windows shell to open the URL

				sts = doURL( alfresco, response.getStatusMessage());
			}

			// Error status

			else {

				//	Get the error message

				String errMsg;

				switch ( response.getStatus()) {
					case StsFileNotFound:
						errMsg = L"File not found";
						break;
					case StsAccessDenied:
						errMsg = L"Access denied";
						break;
					case StsBadParameter:
						errMsg = L"Bad parameter in request";
						break;
					case StsNoSuchAction:
						errMsg = L"No such action";
						break;
					default:
						errMsg = L"Error running action";
						break;
				}

				// Display an error dialog

				CString msg;

				if ( response.hasStatusMessage())
					msg.FormatMessage( L"%1\n\n%2", errMsg.data(), response.getStatusMessage().data());
				else
					msg = errMsg.data();

				AfxMessageBox( msg, MB_OK | MB_ICONERROR);

				DBGOUT_TS << "Action returned error status, " << msg << endl;
			}
		}
		else if ( response.hasStatusMessage()) {

			// Display the message returned by the action

			CString msg;
			msg.FormatMessage( L"Action returned message\n\n%1", response.getStatusMessage().data());
			AfxMessageBox( msg, MB_OK | MB_ICONINFORMATION);

			DBGOUT_TS << "Action returned error message, " << msg << endl;
		}
	}

	// Return the action status

	return sts;
}

/**
 * Launch a command line
 *
 * @param alfresco AlfrescoInterface&
 * @param cmdStr const String&
 * @return bool
 */
bool CAlfrescoApp::doCommandLine( AlfrescoInterface& alfresco, const String& cmdStr) {

	// Check if the command line contains any environment variables/tokens

	String cmdLine = cmdStr;
	int pos = cmdLine.indexOf( L'%');

	if ( pos != -1) {

		// Command line contains environment variables or other tokens that must be replaced

		String newCmdLine = L"";
		if (pos > 0)
			newCmdLine = cmdLine.substring( 0, pos);

		wchar_t envBuf[256];
		size_t envLen;

		while ( pos != -1) {

			// Find the end of the current token

			int endPos = cmdLine.indexOf ( L'%', pos + 1);

			if ( endPos == -1) {
				CString msg;
				msg.FormatMessage( L"Bad token in command line\n\n%1", cmdLine.data());
				AfxMessageBox( msg, MB_OK | MB_ICONERROR);

				return false;
			}

			// Extract the token

			String token = cmdLine.substring( pos + 1, endPos);

			// Replace the token with an environment variable value or other values

			if ( token.equals( L"AlfrescoDir")) {

				// Use the local path to the Alfresco folder that the application is running from

				newCmdLine.append( alfresco.getUNCPath());
			}
			else {

				// Find the environment variable value

				envLen = sizeof( envBuf)/sizeof(wchar_t);
				const wchar_t* pEnvName = token.data();

				if ( _wgetenv_s( &envLen, envBuf, envLen, pEnvName) == 0) {

					// Append the environment variable value

					newCmdLine.append( envBuf);
				}
				else {

					// Error converting the environment variable

					CString msg;
					msg.FormatMessage( L"Failed to convert environment variable\n\n%1\n\n%2", token.data(), cmdLine.data());
					AfxMessageBox( msg, MB_OK | MB_ICONERROR);

					return false;
				}
			}

			// Update the token search position

			pos = endPos + 1;

			if (( unsigned int) pos < cmdStr.length()) {

				// Search for the next token

				pos = cmdLine.indexOf( L'%', pos);
			}
			else {

				// End of string, finish the token search

				pos = -1;
			}

			// Append the normal string between tokens

			if ( pos > (endPos + 1)) {

				// Get the between token sting

				String filler = cmdLine.substring( endPos + 1, pos);
				newCmdLine.append( filler);
			}
			else if ( pos == -1) {

				// Append the remaining string

				String filler = cmdLine.substring( endPos + 1);
				newCmdLine.append( filler);
			}
		}

		// Update the command line

		cmdLine = newCmdLine;
	}

	// Initialize the startup information

	STARTUPINFO startupInfo;
	memset(&startupInfo, 0, sizeof(STARTUPINFO));

	// Launch a process using the command line

	PROCESS_INFORMATION processInfo;
	memset(&processInfo, 0, sizeof(PROCESS_INFORMATION));

	bool sts = false;

	if ( CreateProcess( NULL, (LPWSTR) cmdLine.data(), NULL, NULL, true, 0, NULL, NULL,
			&startupInfo, &processInfo) == false) {
		CString msg;
		msg.FormatMessage( L"Failed to launch command line\n\n%1\n\nError %2!d!", cmdLine.data(), GetLastError());
		AfxMessageBox( msg, MB_OK | MB_ICONERROR);

		// DEBUG

		DBGOUT_TS << "Error, failed to launch command line, status " << GetLastError() << endl;
	}
	else
		sts = true;

	return sts;
}

/**
 * Browse to a URL
 *
 * @param alfresco AlfrescoInterface&
 * @param url const String&
 * @return bool
 */
bool CAlfrescoApp::doURL( AlfrescoInterface& alfresco, const String& url) {

	// Use the Windows shell to open the URL

	bool sts = false;

	HINSTANCE shellSts = ShellExecute( NULL, NULL, url.data(), NULL, NULL, SW_SHOWNORMAL);
	if (( int) shellSts < 32) {
		CString msg;
		msg.FormatMessage( L"Failed to launch URL\n\n%1", url.data());
		AfxMessageBox( msg, MB_OK | MB_ICONERROR);
	}
	else
		sts = true;

	return sts;
}
