/**
 * @file llappviewermacosx.cpp
 * @brief The LLAppViewerMacOSX class definitions
 *
 * $LicenseInfo:firstyear=2007&license=viewergpl$
 * 
 * Copyright (c) 2007-2009, Linden Research, Inc.
 * 
 * Second Life Viewer Source Code
 * The source code in this file ("Source Code") is provided by Linden Lab
 * to you under the terms of the GNU General Public License, version 2.0
 * ("GPL"), unless you have obtained a separate licensing agreement
 * ("Other License"), formally executed by you and Linden Lab.  Terms of
 * the GPL can be found in doc/GPL-license.txt in this distribution, or
 * online at http://secondlifegrid.net/programs/open_source/licensing/gplv2
 * 
 * There are special exceptions to the terms and conditions of the GPL as
 * it is applied to this Source Code. View the full text of the exception
 * in the file doc/FLOSS-exception.txt in this software distribution, or
 * online at
 * http://secondlifegrid.net/programs/open_source/licensing/flossexception
 * 
 * By copying, modifying or distributing this software, you acknowledge
 * that you have read and understood your obligations described above,
 * and agree to abide by those obligations.
 * 
 * ALL LINDEN LAB SOURCE CODE IS PROVIDED "AS IS." LINDEN LAB MAKES NO
 * WARRANTIES, EXPRESS, IMPLIED OR OTHERWISE, REGARDING ITS ACCURACY,
 * COMPLETENESS OR PERFORMANCE.
 * $/LicenseInfo$
 */ 

#include "llviewerprecompiledheaders.h"

#if !defined LL_DARWIN
	#error "Use only with Mac OS X"
#endif

#include "llappviewermacosx.h"
#include "llcommandlineparser.h"

#include "llmemtype.h"

#include "llviewernetwork.h"
#include "llviewercontrol.h"
#include "llmd5.h"
#include "llurlsimstring.h"
#include "llfloaterworldmap.h"
#include "llurldispatcher.h"
#include <Carbon/Carbon.h>
#include "lldir.h"
#include <signal.h>
#include <CoreAudio/CoreAudio.h>	// for systemwide mute
class LLMediaCtrl;		// for LLURLDispatcher

namespace 
{
	// The command line args stored.
	// They are not used immediately by the app.
	int gArgC;
	char** gArgV;
	
	bool sCrashReporterIsRunning = false;
	
	OSErr AEQuitHandler(const AppleEvent *messagein, AppleEvent *reply, long refIn)
	{
		OSErr result = noErr;
		
		LLAppViewer::instance()->userQuit();
		
		return(result);
	}
}

int main( int argc, char **argv ) 
{
	LLMemType mt1(LLMemType::MTYPE_STARTUP);

#if LL_SOLARIS && defined(__sparc)
	asm ("ta\t6");		 // NOTE:  Make sure memory alignment is enforced on SPARC
#endif

	// Set the working dir to <bundle>/Contents/Resources
	if (chdir(gDirUtilp->getAppRODataDir().c_str()) == -1)
	{
		llwarns << "Could not change directory to "
				<< gDirUtilp->getAppRODataDir() << ": " << strerror(errno)
				<< llendl;
	}

	LLAppViewerMacOSX* viewer_app_ptr = new LLAppViewerMacOSX();

	viewer_app_ptr->setErrorHandler(LLAppViewer::handleViewerCrash);

	// Store off the command line args for use later.
	gArgC = argc;
	gArgV = argv;
	
	bool ok = viewer_app_ptr->init();
	if(!ok)
	{
		llwarns << "Application init failed." << llendl;
		return -1;
	}

		// Run the application main loop
	if(!LLApp::isQuitting()) 
	{
		viewer_app_ptr->mainLoop();
	}

	if (!LLApp::isError())
	{
		//
		// We don't want to do cleanup here if the error handler got called -
		// the assumption is that the error handler is responsible for doing
		// app cleanup if there was a problem.
		//
		viewer_app_ptr->cleanup();
	}
	delete viewer_app_ptr;
	viewer_app_ptr = NULL;
	return 0;
}

LLAppViewerMacOSX::LLAppViewerMacOSX()
{
}

LLAppViewerMacOSX::~LLAppViewerMacOSX()
{
}

bool LLAppViewerMacOSX::init()
{
	return LLAppViewer::init();
}

// MacOSX may add and addition command line arguement for the process serial number.
// The option takes a form like '-psn_0_12345'. The following method should be able to recognize
// and either ignore or return a pair of values for the option.
// look for this method to be added to the parser in parseAndStoreResults.
std::pair<std::string, std::string> parse_psn(const std::string& s)
{
    if (s.find("-psn_") == 0) 
	{
		// *FIX:Mani Not sure that the value makes sense.
		// fix it once the actual -psn_XXX syntax is known.
		return std::make_pair("psn", s.substr(5));
    }
	else 
	{
        return std::make_pair(std::string(), std::string());
    }
}

bool LLAppViewerMacOSX::initParseCommandLine(LLCommandLineParser& clp)
{
	// The next two lines add the support for parsing the mac -psn_XXX arg.
	clp.addOptionDesc("psn", NULL, 1, "MacOSX process serial number");
	clp.setCustomParser(parse_psn);
	
    // First read in the args from arguments txt.
    const char* filename = "arguments.txt";
	llifstream ifs(filename, llifstream::binary);
	if (!ifs.is_open())
	{
		llwarns << "Unable to open file" << filename << llendl;
		return false;
	}
	
	if(clp.parseCommandLineFile(ifs) == false)
	{
		return false;
	}

	// Then parse the user's command line, so that any --url arg can appear last
	// Succesive calls to clp.parse... will NOT override earlier options. 
	if(clp.parseCommandLine(gArgC, gArgV) == false)
	{
		return false;
	}
    	
	// Get the user's preferred language string based on the Mac OS localization mechanism.
	// To add a new localization:
		// go to the "Resources" section of the project
		// get info on "language.txt"
		// in the "General" tab, click the "Add Localization" button
		// create a new localization for the language you're adding
		// set the contents of the new localization of the file to the string corresponding to our localization
		//   (i.e. "en", "ja", etc.  Use the existing ones as a guide.)
	CFURLRef url = CFBundleCopyResourceURL(CFBundleGetMainBundle(), CFSTR("language"), CFSTR("txt"), NULL);
	char path[MAX_PATH];
	if(CFURLGetFileSystemRepresentation(url, false, (UInt8 *)path, sizeof(path)))
	{
		std::string lang;
		if(_read_file_into_string(lang, path))		/* Flawfinder: ignore*/
		{
            LLControlVariable* c = gSavedSettings.getControl("SystemLanguage");
            if(c)
            {
                c->setValue(lang, false);
            }
		}
	}
	CFRelease(url);
	
    return true;
}

// *FIX:Mani It would be nice to provide a clean interface to get the
// default_unix_signal_handler for the LLApp class.
extern void default_unix_signal_handler(int, siginfo_t *, void *);
bool LLAppViewerMacOSX::restoreErrorTrap()
{
	// This method intends to reinstate signal handlers.
	// *NOTE:Mani It was found that the first execution of a shader was overriding
	// our initial signal handlers somehow.
	// This method will be called (at least) once per mainloop execution.
	// *NOTE:Mani The signals used below are copied over from the 
	// setup_signals() func in LLApp.cpp
	// LLApp could use some way of overriding that func, but for this viewer
	// fix I opt to avoid affecting the server code.
	
	// Set up signal handlers that may result in program termination
	//
	struct sigaction act;
	struct sigaction old_act;
	act.sa_sigaction = default_unix_signal_handler;
	sigemptyset( &act.sa_mask );
	act.sa_flags = SA_SIGINFO;
	
	unsigned int reset_count = 0;
	
#define SET_SIG(S) 	sigaction(SIGABRT, &act, &old_act); \
					if((unsigned int)act.sa_sigaction != (unsigned int) old_act.sa_sigaction) \
						++reset_count;
	// Synchronous signals
	SET_SIG(SIGABRT)
	SET_SIG(SIGALRM)
	SET_SIG(SIGBUS)
	SET_SIG(SIGFPE)
	SET_SIG(SIGHUP) 
	SET_SIG(SIGILL)
	SET_SIG(SIGPIPE)
	SET_SIG(SIGSEGV)
	SET_SIG(SIGSYS)
	
	SET_SIG(LL_HEARTBEAT_SIGNAL)
	SET_SIG(LL_SMACKDOWN_SIGNAL)
	
	// Asynchronous signals that are normally ignored
	SET_SIG(SIGCHLD)
	SET_SIG(SIGUSR2)
	
	// Asynchronous signals that result in attempted graceful exit
	SET_SIG(SIGHUP)
	SET_SIG(SIGTERM)
	SET_SIG(SIGINT)
	
	// Asynchronous signals that result in core
	SET_SIG(SIGQUIT)	
#undef SET_SIG
	
	return reset_count == 0;
}

void LLAppViewerMacOSX::handleSyncCrashTrace()
{
	// do nothing
}

static OSStatus CarbonEventHandler(EventHandlerCallRef inHandlerCallRef, 
								   EventRef inEvent, 
								   void* inUserData)
{
    ProcessSerialNumber psn;
	
    GetEventParameter(inEvent, 
					  kEventParamProcessID, 
					  typeProcessSerialNumber, 
					  NULL, 
					  sizeof(psn), 
					  NULL, 
					  &psn);
	
    if( GetEventKind(inEvent) == kEventAppTerminated ) 
	{
		Boolean matching_psn = FALSE;	
		OSErr os_result = SameProcess(&psn, (ProcessSerialNumber*)inUserData, &matching_psn);
		if(os_result >= 0 && matching_psn)
		{
			sCrashReporterIsRunning = false;
		}
    }
    return noErr;
}

void LLAppViewerMacOSX::handleCrashReporting(bool reportFreeze)
{
	// This used to use fork&exec, but is switched to LSOpenApplication to 
	// Make sure the crash reporter launches in front of the SL window.
	
	std::string command_str;
	//command_str = "open Second Life.app/Contents/Resources/mac-crash-logger.app";
	command_str = "mac-crash-logger.app/Contents/MacOS/mac-crash-logger";
	
	FSRef appRef;
	Boolean isDir = 0;
	OSStatus os_result = FSPathMakeRef((UInt8*)command_str.c_str(),
									   &appRef,
									   &isDir);
	if(os_result >= 0)
	{
		LSApplicationParameters appParams;
		memset(&appParams, 0, sizeof(appParams));
	 	appParams.version = 0;
		appParams.flags = kLSLaunchNoParams | kLSLaunchStartClassic;
		appParams.application = &appRef;
		
		if(reportFreeze)
		{
			// Make sure freeze reporting launches the crash logger synchronously, lest 
			// Log files get changed by SL while the logger is running.
		
			// *NOTE:Mani A better way - make a copy of the data that the crash reporter will send
			// and let SL go about its business. This way makes the mac work like windows and linux
			// and is the smallest patch for the issue. 
			sCrashReporterIsRunning = true;
			ProcessSerialNumber o_psn;

			static EventHandlerRef sCarbonEventsRef = NULL;
			static const EventTypeSpec kEvents[] = 
			{
				{ kEventClassApplication, kEventAppTerminated }
			};
			
			// Install the handler to detect crash logger termination
			InstallEventHandler(GetApplicationEventTarget(), 
								(EventHandlerUPP) CarbonEventHandler,
								GetEventTypeCount(kEvents),
								kEvents,
								&o_psn,
								&sCarbonEventsRef
								);
			
			// Remove, temporarily the quit handler - which has *crash* behavior before 
			// the mainloop gets running!
			AERemoveEventHandler(kCoreEventClass, 
								 kAEQuitApplication, 
								 NewAEEventHandlerUPP(AEQuitHandler),
								 false);

			// Launch the crash reporter.
			os_result = LSOpenApplication(&appParams, &o_psn);
			
			if(os_result >= 0)
			{	
				EventRecord evt;
				while(sCrashReporterIsRunning)
				{
					while(WaitNextEvent(osMask, &evt, 0, NULL))
					{
						// null op!?!
					}
				}
			}	

			// Re-install the apps quit handler.
			AEInstallEventHandler(kCoreEventClass, 
								  kAEQuitApplication, 
								  NewAEEventHandlerUPP(AEQuitHandler),
								  0, 
								  false);
			
			// Remove the crash reporter quit handler.
			RemoveEventHandler(sCarbonEventsRef);
		}
		else
		{
			appParams.flags |= kLSLaunchAsync;
			clear_signals();

			ProcessSerialNumber o_psn;
			os_result = LSOpenApplication(&appParams, &o_psn);
		}
		
	}

	if(!reportFreeze)
	{
		_exit(1);
	}
	
	// TODO:palmer REMOVE THIS VERY SOON.  THIS WILL NOT BE IN VIEWER 2.0
	// Remove the crash stack log from previous executions.
	// Since we've started logging a new instance of the app, we can assume 
	// The old crash stack is invalid for the next crash report.
	char path[MAX_PATH];		
	FSRef folder;
	if(FSFindFolder(kUserDomain, kLogsFolderType, false, &folder) == noErr)
	{
		// folder is an FSRef to ~/Library/Logs/
		if(FSRefMakePath(&folder, (UInt8*)&path, sizeof(path)) == noErr)
		{
			std::string pathname = std::string(path) + std::string("/CrashReporter/");
			std::string mask = "Second Life*";
			std::string file_name;
			while(gDirUtilp->getNextFileInDir(pathname, mask, file_name, false))
			{
				LLFile::remove(pathname + file_name);
			}
		}
	}
	
}

std::string LLAppViewerMacOSX::generateSerialNumber()
{
	char serial_md5[MD5HEX_STR_SIZE];		// Flawfinder: ignore
	serial_md5[0] = 0;

	// JC: Sample code from http://developer.apple.com/technotes/tn/tn1103.html
	CFStringRef serialNumber = NULL;
	io_service_t    platformExpert = IOServiceGetMatchingService(kIOMasterPortDefault,
																 IOServiceMatching("IOPlatformExpertDevice"));
	if (platformExpert) {
		serialNumber = (CFStringRef) IORegistryEntryCreateCFProperty(platformExpert,
																	 CFSTR(kIOPlatformSerialNumberKey),
																	 kCFAllocatorDefault, 0);		
		IOObjectRelease(platformExpert);
	}
	
	if (serialNumber)
	{
		char buffer[MAX_STRING];		// Flawfinder: ignore
		if (CFStringGetCString(serialNumber, buffer, MAX_STRING, kCFStringEncodingASCII))
		{
			LLMD5 md5( (unsigned char*)buffer );
			md5.hex_digest(serial_md5);
		}
		CFRelease(serialNumber);
	}

	return serial_md5;
}

static AudioDeviceID get_default_audio_output_device(void)
{
	AudioDeviceID device = 0;
	UInt32 size;
	OSStatus err;
	
	size = sizeof(device);
	err = AudioHardwareGetProperty(kAudioHardwarePropertyDefaultOutputDevice, &size, &device);
	if(err != noErr)
	{
		LL_DEBUGS("SystemMute") << "Couldn't get default audio output device (0x" << std::hex << err << ")" << LL_ENDL;
	}
	
	return device;
}

//virtual
void LLAppViewerMacOSX::setMasterSystemAudioMute(bool new_mute)
{
	AudioDeviceID device = get_default_audio_output_device();
	
	if(device != 0)
	{
		UInt32 mute = new_mute;
		OSStatus err = AudioDeviceSetProperty(device, NULL, 0, false, kAudioDevicePropertyMute, sizeof(mute), &mute);
		if(err != noErr)
		{
			LL_INFOS("SystemMute") << "Couldn't set audio mute property (0x" << std::hex << err << ")" << LL_ENDL;
		}
	}
}

//virtual
bool LLAppViewerMacOSX::getMasterSystemAudioMute()
{
	// Assume the system isn't muted 
	UInt32 mute = 0;
	
	AudioDeviceID device = get_default_audio_output_device();
	
	if(device != 0)
	{
		UInt32 size = sizeof(mute);
		OSStatus err = AudioDeviceGetProperty(device, 0, false, kAudioDevicePropertyMute, &size, &mute);
		if(err != noErr)
		{
			LL_DEBUGS("SystemMute") << "Couldn't get audio mute property (0x" << std::hex << err << ")" << LL_ENDL;
		}
	}
	
	return (mute != 0);
}

OSErr AEGURLHandler(const AppleEvent *messagein, AppleEvent *reply, long refIn)
{
	OSErr result = noErr;
	DescType actualType;
	char buffer[1024];		// Flawfinder: ignore
	Size size;
	
	result = AEGetParamPtr (
		messagein,
		keyDirectObject,
		typeCString,
		&actualType,
		(Ptr)buffer,
		sizeof(buffer),
		&size);	
	
	if(result == noErr)
	{
		std::string url = buffer;
		
		// Safari 3.2 silently mangles secondlife:///app/ URLs into
		// secondlife:/app/ (only one leading slash).
		// Fix them up to meet the URL specification. JC
		const std::string prefix = "secondlife:/app/";
		std::string test_prefix = url.substr(0, prefix.length());
		LLStringUtil::toLower(test_prefix);
		if (test_prefix == prefix)
		{
			url.replace(0, prefix.length(), "secondlife:///app/");
		}
		
		LLMediaCtrl* web = NULL;
		const bool trusted_browser = false;
		LLURLDispatcher::dispatch(url, web, trusted_browser);
	}
	
	return(result);
}

OSStatus simpleDialogHandler(EventHandlerCallRef handler, EventRef event, void *userdata)
{
	OSStatus result = eventNotHandledErr;
	OSStatus err;
	UInt32 evtClass = GetEventClass(event);
	UInt32 evtKind = GetEventKind(event);
	WindowRef window = (WindowRef)userdata;
	
	if((evtClass == kEventClassCommand) && (evtKind == kEventCommandProcess))
	{
		HICommand cmd;
		err = GetEventParameter(event, kEventParamDirectObject, typeHICommand, NULL, sizeof(cmd), NULL, &cmd);
		
		if(err == noErr)
		{
			switch(cmd.commandID)
			{
				case kHICommandOK:
					QuitAppModalLoopForWindow(window);
					result = noErr;
				break;
				
				case kHICommandCancel:
					QuitAppModalLoopForWindow(window);
					result = userCanceledErr;
				break;
			}
		}
	}
	
	return(result);
}

void init_apple_menu(const char* product)
{
	// Load up a proper menu bar.
	{
		OSStatus err;
		IBNibRef nib = NULL;
		// NOTE: DO NOT translate or brand this string.  It's an internal name in the .nib file, and MUST match exactly.
		err = CreateNibReference(CFSTR("SecondLife"), &nib);
		
		if(err == noErr)
		{
			// NOTE: DO NOT translate or brand this string.  It's an internal name in the .nib file, and MUST match exactly.
			SetMenuBarFromNib(nib, CFSTR("MenuBar"));
		}

		if(nib != NULL)
		{
			DisposeNibReference(nib);
		}
	}
	
	// Install a handler for 'gurl' AppleEvents.  This is how secondlife:// URLs get passed to the viewer.
	
	if(AEInstallEventHandler('GURL', 'GURL', NewAEEventHandlerUPP(AEGURLHandler),0, false) != noErr)
	{
		// Couldn't install AppleEvent handler.  This error shouldn't be fatal.
		llinfos << "Couldn't install 'GURL' AppleEvent handler.  Continuing..." << llendl;
	}

	// Install a handler for 'quit' AppleEvents.  This makes quitting the application from the dock work.
	if(AEInstallEventHandler(kCoreEventClass, kAEQuitApplication, NewAEEventHandlerUPP(AEQuitHandler),0, false) != noErr)
	{
		// Couldn't install AppleEvent handler.  This error shouldn't be fatal.
		llinfos << "Couldn't install Quit AppleEvent handler.  Continuing..." << llendl;
	}
}