// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// On Mac, shortcuts can't have command-line arguments. Instead, produce small
// app bundles which locate the Chromium framework and load it, passing the
// appropriate data. This is the code for such an app bundle. It should be kept
// minimal and do as little work as possible (with as much work done on
// framework side as possible).

#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <CoreFoundation/CoreFoundation.h>
#import <Foundation/Foundation.h>

#include "chrome/common/app_mode_common_mac.h"

namespace {

// Checks that condition |x| is true. If not, outputs |msg| (together with
// source filename and line number) and exits.
#define CHECK_MSG(x, msg) if (!(x)) check_msg_helper(__FILE__, __LINE__, msg);
void check_msg_helper(const char* file, int line, const char* msg) {
  fprintf(stderr, "%s (%d): %s\n", file, line, msg);
  exit(1);
}

// Converts an NSString to a UTF8 C string (which is allocated, and may be freed
// using |free()|). If |s| is nil or can't produce such a string, this returns
// |NULL|.
char* NSStringToUTF8CString(NSString* s) {
  CHECK_MSG([s isKindOfClass:[NSString class]], "expected an NSString");
  const char* cstring = [s UTF8String];
  return cstring ? strdup(cstring) : NULL;
}

// Converts an NSString to a file-system representation C string (which is
// allocated, and may be freed using |free()|). If |s| is nil or can't produce
// such a string, this returns |NULL|.
char* NSStringToFSCString(NSString* s) {
  CHECK_MSG([s isKindOfClass:[NSString class]], "expected an NSString");
  const char* cstring = [s fileSystemRepresentation];
  return cstring ? strdup(cstring) : NULL;
}

}  // namespace

__attribute__((visibility("default")))
int main(int argc, char** argv) {
  app_mode::ChromeAppModeInfo info;
  info.major_version = 0;  // v0.1
  info.minor_version = 1;
  info.argc = argc;
  info.argv = argv;

  // The Cocoa APIs are a bit more convenient; for this an autorelease pool is
  // needed.
  NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];

  // Get the browser bundle path.
  // TODO(viettrungluu): more fun
  NSString* cr_bundle_path =
      [(NSString*)CFPreferencesCopyAppValue(
          app_mode::kLastRunAppBundlePathPrefsKey,
          app_mode::kAppPrefsID) autorelease];
  CHECK_MSG(cr_bundle_path, "couldn't get browser bundle path");

  // Get the browser bundle.
  NSBundle* cr_bundle = [NSBundle bundleWithPath:cr_bundle_path];
  CHECK_MSG(cr_bundle, "couldn't get browser bundle");

  // Get the current browser version.
  NSString* cr_version =
      [cr_bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
  CHECK_MSG(cr_version, "couldn't get browser version");

  // Get the current browser versioned directory.
  NSArray* cr_versioned_path_components =
      [NSArray arrayWithObjects:cr_bundle_path,
                                @"Contents",
                                @"Versions",
                                cr_version,
                                nil];
  NSString* cr_versioned_path =
      [[NSString pathWithComponents:cr_versioned_path_components]
          stringByStandardizingPath];
  CHECK_MSG(cr_versioned_path, "couldn't get browser versioned path");
  // And copy it, since |cr_versioned_path| will go away with the pool.
  info.chrome_versioned_path = NSStringToFSCString(cr_versioned_path);

  // Get the current main bundle, i.e., that of the app loader that's running.
  NSBundle* app_bundle = [NSBundle mainBundle];
  CHECK_MSG(app_bundle, "couldn't get loader bundle");
  // Optional, so okay if it's NULL.
  info.app_mode_bundle_path = NSStringToFSCString([app_bundle bundlePath]);

  // Read information about the this app shortcut from the Info.plist.
  // Don't check for null-ness on optional items.
  NSDictionary* info_plist = [app_bundle infoDictionary];
  CHECK_MSG(info_plist, "couldn't get loader Info.plist");

  info.app_mode_id = NSStringToUTF8CString(
      [info_plist objectForKey:@"CrAppModeShortcutID"]);
  CHECK_MSG(info.app_mode_id, "couldn't get app shortcut ID");

  info.app_mode_short_name = NSStringToUTF8CString(
      [info_plist objectForKey:@"CrAppModeShortcutShortName"]);

  info.app_mode_name = NSStringToUTF8CString(
      [info_plist objectForKey:@"CrAppModeShortcutName"]);

  info.app_mode_url = NSStringToUTF8CString(
      [info_plist objectForKey:@"CrAppModeShortcutURL"]);
  CHECK_MSG(info.app_mode_url, "couldn't get app shortcut URL");

  // Get the framework path.
  NSString* cr_bundle_exe =
      [cr_bundle objectForInfoDictionaryKey:@"CFBundleExecutable"];
  NSString* cr_framework_path =
      [cr_versioned_path stringByAppendingPathComponent:
          [cr_bundle_exe stringByAppendingString:@" Framework.framework"]];
  cr_framework_path =
      [cr_framework_path stringByAppendingPathComponent:
          [cr_bundle_exe stringByAppendingString:@" Framework"]];

  // Open the framework.
  void* cr_dylib = dlopen([cr_framework_path fileSystemRepresentation],
                          RTLD_LAZY);
  CHECK_MSG(cr_dylib, "couldn't load framework");

  // Drain the pool as late as possible.
  [pool drain];

  typedef int (*StartFun)(const app_mode::ChromeAppModeInfo*);
  StartFun ChromeAppModeStart = (StartFun)dlsym(cr_dylib, "ChromeAppModeStart");
  CHECK_MSG(ChromeAppModeStart, "couldn't get entry point");

  // Exit instead of returning to avoid the the removal of |main()| from stack
  // backtraces under tail call optimization.
  int rv = ChromeAppModeStart(&info);
  exit(rv);
}