// Copyright (c) 2012 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.

#import "chrome/browser/mac/dock.h"

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

#include "base/logging.h"
#include "base/mac/launchd.h"
#include "base/mac/mac_logging.h"
#include "base/mac/mac_util.h"
#include "base/mac/scoped_cftyperef.h"
#include "base/mac/scoped_nsautorelease_pool.h"
#include "base/strings/sys_string_conversions.h"

extern "C" {

// Undocumented private internal CFURL functions. The Dock uses these to
// serialize and deserialize CFURLs for use in its plist's file-data keys. See
// 10.5.8 CF-476.19 and 10.7.2 CF-635.15's CFPriv.h and CFURL.c. The property
// list representation will contain, at the very least, the _CFURLStringType
// and _CFURLString keys. _CFURLStringType is a number that defines the
// interpretation of the _CFURLString. It may be a CFURLPathStyle value, or
// the CFURL-internal FULL_URL_REPRESENTATION value (15). Prior to Mac OS X
// 10.7.2, the Dock plist always used kCFURLPOSIXPathStyle (0), formatting
// _CFURLString as a POSIX path. In Mac OS X 10.7.2 (CF-635.15), it uses
// FULL_URL_REPRESENTATION along with a file:/// URL. This is due to a change
// in _CFURLInit.

CFPropertyListRef _CFURLCopyPropertyListRepresentation(CFURLRef url);
CFURLRef _CFURLCreateFromPropertyListRepresentation(
    CFAllocatorRef allocator, CFPropertyListRef property_list_representation);

}  // extern "C"

namespace dock {
namespace {

NSString* const kDockTileDataKey = @"tile-data";
NSString* const kDockFileDataKey = @"file-data";

// A wrapper around _CFURLCopyPropertyListRepresentation that operates on
// Foundation data types and returns an autoreleased NSDictionary.
NSDictionary* NSURLCopyDictionary(NSURL* url) {
  CFURLRef url_cf = base::mac::NSToCFCast(url);
  base::ScopedCFTypeRef<CFPropertyListRef> property_list(
      _CFURLCopyPropertyListRepresentation(url_cf));
  CFDictionaryRef dictionary_cf =
      base::mac::CFCast<CFDictionaryRef>(property_list);
  NSDictionary* dictionary = base::mac::CFToNSCast(dictionary_cf);

  if (!dictionary) {
    return nil;
  }

  NSMakeCollectable(property_list.release());
  return [dictionary autorelease];
}

// A wrapper around _CFURLCreateFromPropertyListRepresentation that operates
// on Foundation data types and returns an autoreleased NSURL.
NSURL* NSURLCreateFromDictionary(NSDictionary* dictionary) {
  CFDictionaryRef dictionary_cf = base::mac::NSToCFCast(dictionary);
  base::ScopedCFTypeRef<CFURLRef> url_cf(
      _CFURLCreateFromPropertyListRepresentation(NULL, dictionary_cf));
  NSURL* url = base::mac::CFToNSCast(url_cf);

  if (!url) {
    return nil;
  }

  NSMakeCollectable(url_cf.release());
  return [url autorelease];
}

// Returns an array parallel to |persistent_apps| containing only the
// pathnames of the Dock tiles contained therein. Returns nil on failure, such
// as when the structure of |persistent_apps| is not understood.
NSMutableArray* PersistentAppPaths(NSArray* persistent_apps) {
  NSMutableArray* app_paths =
      [NSMutableArray arrayWithCapacity:[persistent_apps count]];

  for (NSDictionary* app in persistent_apps) {
    if (![app isKindOfClass:[NSDictionary class]]) {
      LOG(ERROR) << "app not NSDictionary";
      return nil;
    }

    NSDictionary* tile_data = [app objectForKey:kDockTileDataKey];
    if (![tile_data isKindOfClass:[NSDictionary class]]) {
      LOG(ERROR) << "tile_data not NSDictionary";
      return nil;
    }

    NSDictionary* file_data = [tile_data objectForKey:kDockFileDataKey];
    if (![file_data isKindOfClass:[NSDictionary class]]) {
      // Some apps (e.g. Dashboard) have no file data, but instead have a
      // special value for the tile-type key. For these, add an empty string to
      // align indexes with the source array.
      [app_paths addObject:@""];
      continue;
    }

    NSURL* url = NSURLCreateFromDictionary(file_data);
    if (!url) {
      LOG(ERROR) << "no URL";
      return nil;
    }

    if (![url isFileURL]) {
      LOG(ERROR) << "non-file URL";
      return nil;
    }

    NSString* path = [url path];
    [app_paths addObject:path];
  }

  return app_paths;
}

// Restart the Dock process by sending it a SIGHUP.
void Restart() {
  // Doing this via launchd using the proper job label is the safest way to
  // handle the restart. Unlike "killall Dock", looking this up via launchd
  // guarantees that only the right process will be targeted.
  pid_t pid = base::mac::PIDForJob("com.apple.Dock.agent");
  if (pid <= 0) {
    return;
  }

  // Sending a SIGHUP to the Dock seems to be a more reliable way to get the
  // replacement Dock process to read the newly written plist than using the
  // equivalent of "launchctl stop" (even if followed by "launchctl start.")
  // Note that this is a potential race in that pid may no longer be valid or
  // may even have been reused.
  kill(pid, SIGHUP);
}

}  // namespace

AddIconStatus AddIcon(NSString* installed_path, NSString* dmg_app_path) {
  // ApplicationServices.framework/Frameworks/HIServices.framework contains an
  // undocumented function, CoreDockAddFileToDock, that is able to add items
  // to the Dock "live" without requiring a Dock restart. Under the hood, it
  // communicates with the Dock via Mach IPC. It is available as of Mac OS X
  // 10.6. AddIcon could call CoreDockAddFileToDock if available, but
  // CoreDockAddFileToDock seems to always to add the new Dock icon last,
  // where AddIcon takes care to position the icon appropriately. Based on
  // disassembly, the signature of the undocumented function appears to be
  //    extern "C" OSStatus CoreDockAddFileToDock(CFURLRef url, int);
  // The int argument doesn't appear to have any effect. It's not used as the
  // position to place the icon as hoped.

  // There's enough potential allocation in this function to justify a
  // distinct pool.
  base::mac::ScopedNSAutoreleasePool autorelease_pool;

  NSString* const kDockDomain = @"com.apple.dock";
  NSUserDefaults* user_defaults = [NSUserDefaults standardUserDefaults];

  NSDictionary* dock_plist_const =
      [user_defaults persistentDomainForName:kDockDomain];
  if (![dock_plist_const isKindOfClass:[NSDictionary class]]) {
    LOG(ERROR) << "dock_plist_const not NSDictionary";
    return IconAddFailure;
  }
  NSMutableDictionary* dock_plist =
      [NSMutableDictionary dictionaryWithDictionary:dock_plist_const];

  NSString* const kDockPersistentAppsKey = @"persistent-apps";
  NSArray* persistent_apps_const =
      [dock_plist objectForKey:kDockPersistentAppsKey];
  if (![persistent_apps_const isKindOfClass:[NSArray class]]) {
    LOG(ERROR) << "persistent_apps_const not NSArray";
    return IconAddFailure;
  }
  NSMutableArray* persistent_apps =
      [NSMutableArray arrayWithArray:persistent_apps_const];

  NSMutableArray* persistent_app_paths = PersistentAppPaths(persistent_apps);
  if (!persistent_app_paths) {
    return IconAddFailure;
  }

  NSUInteger already_installed_app_index = NSNotFound;
  NSUInteger app_index = NSNotFound;
  for (NSUInteger index = 0; index < [persistent_apps count]; ++index) {
    NSString* app_path = [persistent_app_paths objectAtIndex:index];
    if ([app_path isEqualToString:installed_path]) {
      // If the Dock already contains a reference to the newly installed
      // application, don't add another one.
      already_installed_app_index = index;
    } else if ([app_path isEqualToString:dmg_app_path]) {
      // If the Dock contains a reference to the application on the disk
      // image, replace it with a reference to the newly installed
      // application. However, if the Dock contains a reference to both the
      // application on the disk image and the newly installed application,
      // just remove the one referencing the disk image.
      //
      // This case is only encountered when the user drags the icon from the
      // disk image volume window in the Finder directly into the Dock.
      app_index = index;
    }
  }

  bool made_change = false;

  if (app_index != NSNotFound) {
    // Remove the Dock's reference to the application on the disk image.
    [persistent_apps removeObjectAtIndex:app_index];
    [persistent_app_paths removeObjectAtIndex:app_index];
    made_change = true;
  }

  if (already_installed_app_index == NSNotFound) {
    // The Dock doesn't yet have a reference to the icon at the
    // newly installed path. Figure out where to put the new icon.
    NSString* app_name = [installed_path lastPathComponent];

    if (app_index == NSNotFound) {
      // If an application with this name is already in the Dock, put the new
      // one right before it.
      for (NSUInteger index = 0; index < [persistent_apps count]; ++index) {
        NSString* dock_app_name =
            [[persistent_app_paths objectAtIndex:index] lastPathComponent];
        if ([dock_app_name isEqualToString:app_name]) {
          app_index = index;
          break;
        }
      }
    }

#if defined(GOOGLE_CHROME_BUILD)
    if (app_index == NSNotFound) {
      // If this is an officially-branded Chrome (including Canary) and an
      // application matching the "other" flavor is already in the Dock, put
      // them next to each other. Google Chrome will precede Google Chrome
      // Canary in the Dock.
      NSString* chrome_name = @"Google Chrome.app";
      NSString* canary_name = @"Google Chrome Canary.app";
      for (NSUInteger index = 0; index < [persistent_apps count]; ++index) {
        NSString* dock_app_name =
            [[persistent_app_paths objectAtIndex:index] lastPathComponent];
        if ([dock_app_name isEqualToString:canary_name] &&
            [app_name isEqualToString:chrome_name]) {
          app_index = index;

          // Break: put Google Chrome.app before the first Google Chrome
          // Canary.app.
          break;
        } else if ([dock_app_name isEqualToString:chrome_name] &&
                   [app_name isEqualToString:canary_name]) {
          app_index = index + 1;

          // No break: put Google Chrome Canary.app after the last Google
          // Chrome.app.
        }
      }
    }
#endif  // GOOGLE_CHROME_BUILD

    if (app_index == NSNotFound) {
      // Put the new application after the last browser application already
      // present in the Dock.
      NSArray* other_browser_app_names =
          [NSArray arrayWithObjects:
#if defined(GOOGLE_CHROME_BUILD)
                                    @"Chromium.app",  // Unbranded Google Chrome
#else
                                    @"Google Chrome.app",
                                    @"Google Chrome Canary.app",
#endif
                                    @"Safari.app",
                                    @"Firefox.app",
                                    @"Camino.app",
                                    @"Opera.app",
                                    @"OmniWeb.app",
                                    @"WebKit.app",    // Safari nightly
                                    @"Aurora.app",    // Firefox dev
                                    @"Nightly.app",   // Firefox nightly
                                    nil];
      for (NSUInteger index = 0; index < [persistent_apps count]; ++index) {
        NSString* dock_app_name =
            [[persistent_app_paths objectAtIndex:index] lastPathComponent];
        if ([other_browser_app_names containsObject:dock_app_name]) {
          app_index = index + 1;
        }
      }
    }

    if (app_index == NSNotFound) {
      // Put the new application last in the Dock.
      app_index = [persistent_apps count];
    }

    // Set up the new Dock tile.
    NSURL* url = [NSURL fileURLWithPath:installed_path isDirectory:YES];
    NSDictionary* url_dict = NSURLCopyDictionary(url);
    if (!url_dict) {
      LOG(ERROR) << "couldn't create url_dict";
      return IconAddFailure;
    }

    NSDictionary* new_tile_data =
        [NSDictionary dictionaryWithObject:url_dict
                                    forKey:kDockFileDataKey];
    NSDictionary* new_tile =
        [NSDictionary dictionaryWithObject:new_tile_data
                                    forKey:kDockTileDataKey];

    // Add the new tile to the Dock.
    [persistent_apps insertObject:new_tile atIndex:app_index];
    [persistent_app_paths insertObject:installed_path atIndex:app_index];
    made_change = true;
  }

  // Verify that the arrays are still parallel.
  DCHECK_EQ([persistent_apps count], [persistent_app_paths count]);

  if (!made_change) {
    // If no changes were made, there's no point in rewriting the Dock's
    // plist or restarting the Dock.
    return IconAlreadyPresent;
  }

  // Rewrite the plist.
  [dock_plist setObject:persistent_apps forKey:kDockPersistentAppsKey];
  [user_defaults setPersistentDomain:dock_plist forName:kDockDomain];

  Restart();
  return IconAddSuccess;
}

}  // namespace dock