/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

//PLAN:

 // open my bundle, check for an override binary signature
 // find the newest firefox. open its bundle, get the version number.
 // if the firefox version is different than ours:
 //   delete our own binary,
 //   copy the newer webapprt binary from Firefox
 //   exec it, and quit

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/stat.h>

#include <Cocoa/Cocoa.h>
#include <Foundation/Foundation.h>

#include "nsXPCOMGlue.h"
#include "nsINIParser.h"
#include "nsXPCOMPrivate.h"              // for MAXPATHLEN and XPCOM_DLL
#include "nsXULAppAPI.h"
#include "nsComponentManagerUtils.h"
#include "nsCOMPtr.h"
#include "nsIFile.h"
#include "nsStringGlue.h"
#include "mozilla/AppData.h"

using namespace mozilla;

const char WEBAPPRT_EXECUTABLE[] = "webapprt-stub";
const char FXAPPINI_NAME[] = "application.ini";
const char WEBAPPINI_NAME[] = "webapp.ini";
const char WEBRTINI_NAME[] = "webapprt.ini";

//need the correct relative path here
const char APP_CONTENTS_PATH[] = "/Contents/MacOS/";

//the path to the WebappRT subdir within the Firefox app contents dir
const char WEBAPPRT_PATH[] = "webapprt/";

void ExecNewBinary(NSString* launchPath);

NSString *PathToWebRT(NSString* alternateBinaryID);

NSException* MakeException(NSString* name, NSString* message);

void DisplayErrorAlert(NSString* title, NSString* message);

XRE_GetFileFromPathType XRE_GetFileFromPath;
XRE_CreateAppDataType XRE_CreateAppData;
XRE_FreeAppDataType XRE_FreeAppData;
XRE_mainType XRE_main;

const nsDynamicFunctionLoad kXULFuncs[] = {
  { "XRE_GetFileFromPath", (NSFuncPtr*) &XRE_GetFileFromPath },
  { "XRE_CreateAppData", (NSFuncPtr*) &XRE_CreateAppData },
  { "XRE_FreeAppData", (NSFuncPtr*) &XRE_FreeAppData },
  { "XRE_main", (NSFuncPtr*) &XRE_main },
  { nullptr, nullptr }
};

nsresult
AttemptGRELoad(char *greDir)
{
  nsresult rv;
  char xpcomDLLPath[MAXPATHLEN];
  snprintf(xpcomDLLPath, MAXPATHLEN, "%s%s", greDir, XPCOM_DLL);

  rv = XPCOMGlueStartup(xpcomDLLPath);

  if (NS_FAILED(rv)) {
    return rv;
  }

  rv = XPCOMGlueLoadXULFunctions(kXULFuncs);
  if (NS_FAILED(rv)) {
    return rv;
  }

  return rv;
}

int
main(int argc, char **argv)
{
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

  NSDictionary *args = [[NSUserDefaults standardUserDefaults]
                         volatileDomainForName:NSArgumentDomain];

  NSString *firefoxPath = nil;
  NSString *alternateBinaryID = nil;

  //this is our version, to be compared with the version of the binary we are asked to use
  NSString* myVersion = [NSString stringWithFormat:@"%s", NS_STRINGIFY(GRE_BUILDID)];

  NSLog(@"MY WEBAPPRT BUILDID: %@", myVersion);


  //I need to look in our bundle first, before deciding what firefox binary to use
  NSBundle* myBundle = [NSBundle mainBundle];
  NSString* myBundlePath = [myBundle bundlePath];
  alternateBinaryID = [myBundle objectForInfoDictionaryKey:@"FirefoxBinary"];
  NSLog(@"found override firefox binary: %@", alternateBinaryID);

  @try {
    //find a webapprt binary to launch with.  throws an exception with error dialog if none found.
    firefoxPath = PathToWebRT(alternateBinaryID);
    NSLog(@"USING FIREFOX : %@", firefoxPath);

    NSString *myWebRTPath = [myBundle pathForAuxiliaryExecutable: @"webapprt"];
    if (!myWebRTPath) {
      @throw MakeException(@"Missing Web Runtime Files", @"Cannot locate binary for this App");
    }

    //GET FIREFOX BUILD ID
    NSString *firefoxINIFilePath = [NSString stringWithFormat:@"%@%s%s", firefoxPath, APP_CONTENTS_PATH, FXAPPINI_NAME];
    nsINIParser ffparser;
    NSLog(@"Looking for firefox ini file here: %@", firefoxINIFilePath);

    if (NS_FAILED(ffparser.Init([firefoxINIFilePath UTF8String]))) {
      NSLog(@"Unable to locate Firefox application.ini");
      @throw MakeException(@"Error", @"Unable to parse environment files for application startup");
    }

    char ffVersChars[MAXPATHLEN];
    if (NS_FAILED(ffparser.GetString("App", "BuildID", ffVersChars, MAXPATHLEN))) {
      NSLog(@"Unable to retrieve Firefox BuildID");
      @throw MakeException(@"Error", @"Unable to determine Firefox version.");
    }
    NSString* firefoxVersion = [NSString stringWithFormat:@"%s", ffVersChars];

    NSLog(@"FIREFOX WEBAPPRT BUILDID: %@", firefoxVersion);
    //GOT FIREFOX BUILD ID

    //COMPARE MY BUILD ID AND FIREFOX BUILD ID
    if ([myVersion compare: firefoxVersion] != NSOrderedSame) {
      //we are going to assume that if they are different, we need to re-copy the webapprt, regardless of whether
      // it is newer or older.  If we don't find a webapprt, then the current Firefox must not be new enough to run webapps.
      NSLog(@"### This Application has an old webrt. Updating it.");
      NSLog(@"### My webapprt path: %@", myWebRTPath);

      NSFileManager* fileClerk = [[NSFileManager alloc] init];
      NSError *errorDesc = nil;

      //we know the firefox path, so copy the new webapprt here
      NSString *newWebRTPath = [NSString stringWithFormat: @"%@%s%s", firefoxPath, APP_CONTENTS_PATH, WEBAPPRT_EXECUTABLE];
      NSLog(@"### Firefox webapprt path: %@", newWebRTPath);
      if (![fileClerk fileExistsAtPath:newWebRTPath]) {
        NSString* msg = [NSString stringWithFormat: @"This version of Firefox (%@) cannot run web applications, because it is not recent enough or damaged", firefoxVersion];
        @throw MakeException(@"Missing Web Runtime Files", msg);
      }

      [fileClerk removeItemAtPath: myWebRTPath error: &errorDesc];
      if (errorDesc != nil) {
        NSLog(@"failed to unlink old binary file at path: %@ with error: %@", myWebRTPath, errorDesc);
        @throw MakeException(@"Unable To Update", @"Failed preparation for Web Runtime update");
      }

      [fileClerk copyItemAtPath: newWebRTPath toPath: myWebRTPath error: &errorDesc];
      [fileClerk release];
      if (errorDesc != nil) {
        NSLog(@"failed to copy new webrt file: %@", errorDesc);
        @throw MakeException(@"Unable To Update Web Runtime", @"Failed to update Web Runtime");
      } else {
        NSLog(@"### Successfully updated webapprt, relaunching");
      }

      //execv the new binary, and ride off into the sunset
      ExecNewBinary(myWebRTPath);

    } else {
      //we are ready to load XUL and such, and go go go

      NSLog(@"This Application has the newest webrt.  Launching!");

      int result = 0;
      char rtINIPath[MAXPATHLEN];

      // Set up our environment to know where webapp.ini was loaded from.
      char appEnv[MAXPATHLEN];
      snprintf(appEnv, MAXPATHLEN, "%s%s%s", [myBundlePath UTF8String], APP_CONTENTS_PATH, WEBAPPINI_NAME);
      if (setenv("XUL_APP_FILE", appEnv, 1)) {
        NSLog(@"Couldn't set XUL_APP_FILE to: %s", appEnv);
        @throw MakeException(@"Error", @"Unable to set Web Runtime INI file.");
      }
      NSLog(@"Set XUL_APP_FILE to: %s", appEnv);

      //CONSTRUCT GREDIR AND CALL XPCOMGLUE WITH IT
      char greDir[MAXPATHLEN];
      snprintf(greDir, MAXPATHLEN, "%s%s", [firefoxPath UTF8String], APP_CONTENTS_PATH);
      if (!NS_SUCCEEDED(AttemptGRELoad(greDir))) {
          @throw MakeException(@"Error", @"Unable to load XUL files for application startup");
      }

      // NOTE: The GRE has successfully loaded, so we can use XPCOM now

      NS_LogInit();
      { // Scope for any XPCOM stuff we create

        // Get the path to the runtime directory.
        char rtDir[MAXPATHLEN];
        snprintf(rtDir, MAXPATHLEN, "%s%s%s", [firefoxPath UTF8String], APP_CONTENTS_PATH, WEBAPPRT_PATH);

        // Get the path to the runtime's INI file.  This is in the runtime
        // directory.
        snprintf(rtINIPath, MAXPATHLEN, "%s%s%s%s", [firefoxPath UTF8String], APP_CONTENTS_PATH, WEBAPPRT_PATH, WEBRTINI_NAME);
        NSLog(@"WebappRT application.ini path: %s", rtINIPath);

        // Load the runtime's INI from its path.
        nsCOMPtr<nsIFile> rtINI;
        if (NS_FAILED(XRE_GetFileFromPath(rtINIPath, getter_AddRefs(rtINI)))) {
          NSLog(@"Runtime INI path not recognized: '%s'\n", rtINIPath);
          @throw MakeException(@"Error", @"Incorrect path to base INI file.");
        }

        bool exists;
        nsresult rv = rtINI->Exists(&exists);
        if (NS_FAILED(rv) || !exists) {
          NSString* msg = [NSString stringWithFormat: @"This copy of Firefox (%@) cannot run Apps, because it is missing the Web Runtime's application.ini file", firefoxVersion];
          @throw MakeException(@"Missing Web Runtime application.ini", msg);
        }

        nsXREAppData *webShellAppData;
        if (NS_FAILED(XRE_CreateAppData(rtINI, &webShellAppData))) {
          NSLog(@"Couldn't read WebappRT application.ini: %s", rtINIPath);
          @throw MakeException(@"Error", @"Unable to parse base INI file.");
        }

        NSString *profile = [args objectForKey:@"profile"];
        if (profile) {
          NSLog(@"Profile specified with -profile: %@", profile);
        }
        else {
          nsINIParser parser;
          if (NS_FAILED(parser.Init(appEnv))) {
            NSLog(@"%s was not found\n", appEnv);
            @throw MakeException(@"Error", @"Unable to parse environment files for application startup");
          }
          char profile[MAXPATHLEN];
          if (NS_FAILED(parser.GetString("Webapp", "Profile", profile, MAXPATHLEN))) {
            NSLog(@"Unable to retrieve profile from App INI file");
            @throw MakeException(@"Error", @"Unable to retrieve installation profile.");
          }
          NSLog(@"setting app profile: %s", profile);
          SetAllocatedString(webShellAppData->profile, profile);
        }

        nsCOMPtr<nsIFile> directory;
        if (NS_FAILED(XRE_GetFileFromPath(rtDir, getter_AddRefs(directory)))) {
          NSLog(@"Unable to open app dir");
          @throw MakeException(@"Error", @"Unable to open App directory.");
        }

        nsCOMPtr<nsIFile> xreDir;
        if (NS_FAILED(XRE_GetFileFromPath(greDir, getter_AddRefs(xreDir)))) {
          NSLog(@"Unable to open XRE dir");
          @throw MakeException(@"Error", @"Unable to open App XRE directory.");
        }

        xreDir.forget(&webShellAppData->xreDirectory);
        NS_IF_RELEASE(webShellAppData->directory);
        directory.forget(&webShellAppData->directory);

        // There is only XUL.
        result = XRE_main(argc, argv, webShellAppData, 0);

        XRE_FreeAppData(webShellAppData);
      }
      NS_LogTerm();

      return result;
    }

  }
  @catch (NSException *e) {
    NSLog(@"got exception: %@", e);
    DisplayErrorAlert([e name], [e reason]);
  }
  @finally {
    [pool drain];
  }
  return 0;
}  //end main


NSException*
MakeException(NSString* name, NSString* message)
{
  NSException* myException = [NSException
        exceptionWithName:name
        reason:message
        userInfo:nil];
  return myException;
}

void
DisplayErrorAlert(NSString* title, NSString* message)
{
  CFUserNotificationDisplayNotice(0, kCFUserNotificationNoteAlertLevel,
    NULL, NULL, NULL,
    (CFStringRef)title,
    (CFStringRef)message,
    CFSTR("Quit")
    );
}

/* Find the currently installed Firefox, if any, and return
 * an absolute path to it. may return nil */
NSString
*PathToWebRT(NSString* alternateBinaryID)
{
  //default is firefox
  NSString *binaryPath = nil;

  // We're run from the Firefox bundle during WebappRT chrome and content tests.
  NSString *myBundlePath = [[NSBundle mainBundle] bundlePath];
  NSString *fxPath = [NSString stringWithFormat:@"%@%sfirefox-bin",
                                 myBundlePath, APP_CONTENTS_PATH];
  if ([[NSFileManager defaultManager] fileExistsAtPath:fxPath]) {
    return myBundlePath;
  }

  //we look for these flavors of Firefox, in this order
  NSArray* launchBinarySearchList = [NSArray arrayWithObjects: @"org.mozilla.nightly",
                                                                @"org.mozilla.aurora",
                                                                @"org.mozilla.firefox", nil];

  //if they provided a manual override, use that.  If they made an error, it will fail to launch
  if (alternateBinaryID != nil && ([alternateBinaryID length] > 0)) {
    binaryPath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:alternateBinaryID];
    if (binaryPath == nil || [binaryPath length] == 0) {
      @throw MakeException(@"Web Runtime Not Found",
                            [NSString stringWithFormat:@"Failed to locate specified override Web Runtime with signature '%@'", alternateBinaryID]);
    }
    return binaryPath;
  }

  //No override found, loop through the various flavors of firefox we have
  for (NSString* signature in launchBinarySearchList) {
    NSLog(@"SEARCHING for webapprt, trying: %@", signature);
    binaryPath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:signature];
    if (binaryPath && [binaryPath length] > 0) {
      return binaryPath;
    }
  }

  NSLog(@"unable to find a valid webrt path");
  @throw MakeException(@"This App requires that Firefox version 16 or above is installed.", @"Firefox 16+ has not been detected.");

  return nil;
}

void
ExecNewBinary(NSString* launchPath)
{
  NSLog(@" launching webrt at path: %@\n", launchPath);

  const char *const newargv[] = {[launchPath UTF8String], NULL};

  NSLog(@"COMMAND LINE: '%@ %s'", launchPath, newargv[0]);
  execv([launchPath UTF8String], (char **)newargv);
}
