// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "chrome/common/mac/app_mode_chrome_locator.h"

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

#include <set>

#include "base/apple/bridging.h"
#include "base/apple/foundation_util.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/mac/app_mode_common.h"
#include "third_party/abseil-cpp/absl/types/optional.h"

namespace app_mode {

namespace {

struct PathAndStructure {
  NSString* __strong framework_dylib_path;
  bool is_new_app_structure;
};

absl::optional<PathAndStructure> GetFrameworkDylibPathAndStructure(
    NSString* bundle_path,
    NSString* version) {
  // NEW STYLE:
  // Chromium.app/Contents/Frameworks/Chromium Framework.framework/
  //   Versions/<version>/Chromium Framework
  NSString* path = [NSString pathWithComponents:@[
    bundle_path, @"Contents", @"Frameworks", @(chrome::kFrameworkName),
    @"Versions", version, @(chrome::kFrameworkExecutableName)
  ]];

  if ([NSFileManager.defaultManager fileExistsAtPath:path]) {
    return PathAndStructure{path, true};
  }

  // OLD STYLE:
  // Chromium.app/Contents/Versions/<version>/Chromium Framework.framework/
  //   Versions/A/Chromium Framework
  path = [NSString pathWithComponents:@[
    bundle_path, @"Contents", @"Versions", version, @(chrome::kFrameworkName),
    @"Versions", @"A", @(chrome::kFrameworkExecutableName)
  ]];

  if ([NSFileManager.defaultManager fileExistsAtPath:path]) {
    return PathAndStructure{path, false};
  }

  return absl::nullopt;
}

bool IsPathValidForBundle(const base::FilePath& bundle_path,
                          NSString* bundle_id) {
  if (bundle_path.empty())
    return false;

  if (!base::DirectoryExists(bundle_path))
    return false;

  NSString* ns_bundle_path = base::SysUTF8ToNSString(bundle_path.value());
  NSBundle* bundle = [NSBundle bundleWithPath:ns_bundle_path];
  if (!bundle || ![bundle_id isEqualToString:bundle.bundleIdentifier]) {
    return false;
  }

  return true;
}

}  // namespace

bool FindChromeBundle(NSString* bundle_id, base::FilePath* out_bundle) {
  // Retrieve the last-run Chrome bundle location.
  base::FilePath last_run_bundle_path;
  {
    NSString* cr_bundle_path_ns = base::apple::CFToNSOwnershipCast(
        base::apple::CFCastStrict<CFStringRef>(CFPreferencesCopyAppValue(
            base::apple::NSToCFPtrCast(app_mode::kLastRunAppBundlePathPrefsKey),
            base::apple::NSToCFPtrCast(bundle_id))));
    last_run_bundle_path = base::apple::NSStringToFilePath(cr_bundle_path_ns);
  }

  // Look up running instances of the specified bundle ID.
  {
    // Note that IsPathValidForBundle is guaranteed to be true for all elements
    // in `running_bundle_paths` because runningApplicationsWithBundleIdentifier
    // returned them.
    std::set<base::FilePath> running_bundle_paths;
    NSArray<NSRunningApplication*>* running_applications = [NSRunningApplication
        runningApplicationsWithBundleIdentifier:bundle_id];
    for (NSRunningApplication* running_application : running_applications) {
      base::FilePath bundle_path =
          base::apple::NSURLToFilePath(running_application.bundleURL);
      DCHECK(!bundle_path.empty());
      running_bundle_paths.insert(bundle_path);
    }

    // If the last-run instance is still running, then use that instance.
    if (running_bundle_paths.count(last_run_bundle_path)) {
      *out_bundle = last_run_bundle_path;
      return true;
    }

    // Otherwise, select a running bundle path arbitrarily.
    // TODO(https://crbug.com/1278425): This choice should not be made
    // arbitrarily.
    if (!running_bundle_paths.empty()) {
      *out_bundle = *running_bundle_paths.begin();
      return true;
    }
  }

  // Next, use the last run bundle path, if it is valid.
  if (IsPathValidForBundle(last_run_bundle_path, bundle_id)) {
    *out_bundle = last_run_bundle_path;
    return true;
  }

  // Finally, search the filesystem for a bundle. If several copies of the
  // bundle are present, this will select one arbitrarily.
  {
    // Note that `IsPathValidForBundle` is guaranteed to be true for
    // `bundle_path` because URLForApplicationWithBundleIdentifier returned it.
    NSURL* url = [NSWorkspace.sharedWorkspace
        URLForApplicationWithBundleIdentifier:bundle_id];
    if (url) {
      *out_bundle = base::apple::NSURLToFilePath(url);
      return true;
    }
  }
  return false;
}

bool GetChromeBundleInfo(const base::FilePath& chrome_bundle,
                         const std::string& version_str,
                         base::FilePath* executable_path,
                         base::FilePath* framework_path,
                         base::FilePath* framework_dylib_path) {
  NSString* cr_bundle_path = base::apple::FilePathToNSString(chrome_bundle);
  NSBundle* cr_bundle = [NSBundle bundleWithPath:cr_bundle_path];
  if (!cr_bundle)
    return false;

  // Try to get the version requested, if present.
  absl::optional<PathAndStructure> framework_path_and_structure;
  if (!version_str.empty()) {
    framework_path_and_structure = GetFrameworkDylibPathAndStructure(
        cr_bundle_path, base::SysUTF8ToNSString(version_str));
  }

  // If the version requested is not present, or no specific version was
  // requested, fall back to the "current" version. For new-style bundle
  // structures, use the "Current" symlink. (This will intentionally return nil
  // with the old bundle structure.)
  //
  // Note that the scenario where a specific version was requested but is not
  // present is a "should not happen" scenario. Chromium, while it is running,
  // maintains a link to the currently running version, and this function's
  // caller checked to see if the Chromium was still running. However, even in
  // this bizarre case, it's best to find _some_ Chromium.
  if (!framework_path_and_structure) {
    framework_path_and_structure =
        GetFrameworkDylibPathAndStructure(cr_bundle_path, @"Current");
    if (framework_path_and_structure) {
      framework_path_and_structure->framework_dylib_path =
          [framework_path_and_structure
                  ->framework_dylib_path stringByResolvingSymlinksInPath];
    }
  }

  // At this point it is known that it is an old-style bundle structure (or a
  // rather broken new-style bundle). Try explicitly specifying the version of
  // the framework matching the outer bundle version.
  if (!framework_path_and_structure) {
    NSString* cr_version = base::apple::ObjCCast<NSString>([cr_bundle
        objectForInfoDictionaryKey:app_mode::kCFBundleShortVersionStringKey]);
    if (cr_version) {
      framework_path_and_structure =
          GetFrameworkDylibPathAndStructure(cr_bundle_path, cr_version);
    }
  }

  if (!framework_path_and_structure)
    return false;

  // A few sanity checks.
  BOOL is_directory;
  BOOL exists = [NSFileManager.defaultManager
      fileExistsAtPath:framework_path_and_structure->framework_dylib_path
           isDirectory:&is_directory];
  if (!exists || is_directory)
    return false;

  NSString* cr_framework_path;

  if (framework_path_and_structure->is_new_app_structure) {
    // For the path to the framework version itself, remove the framework name.
    cr_framework_path =
        [framework_path_and_structure
                ->framework_dylib_path stringByDeletingLastPathComponent];
  } else {
    // For the path to the framework itself, remove the framework name ...
    cr_framework_path =
        [framework_path_and_structure
                ->framework_dylib_path stringByDeletingLastPathComponent];
    // ... the "A" ...
    cr_framework_path = [cr_framework_path stringByDeletingLastPathComponent];
    // ... and the "Versions" directory.
    cr_framework_path = [cr_framework_path stringByDeletingLastPathComponent];
  }

  // Everything is OK; copy the output parameters.
  *executable_path = base::apple::NSStringToFilePath(cr_bundle.executablePath);
  *framework_path = base::apple::NSStringToFilePath(cr_framework_path);
  *framework_dylib_path = base::apple::NSStringToFilePath(
      framework_path_and_structure->framework_dylib_path);
  return true;
}

}  // namespace app_mode
