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

#include "chrome/browser/web_applications/locks/web_app_lock_manager.h"

#include <memory>

#include "base/functional/bind.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/to_string.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/task_runner.h"
#include "base/types/pass_key.h"
#include "base/values.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/web_applications/locks/all_apps_lock.h"
#include "chrome/browser/web_applications/locks/app_lock.h"
#include "chrome/browser/web_applications/locks/lock.h"
#include "chrome/browser/web_applications/locks/noop_lock.h"
#include "chrome/browser/web_applications/locks/partitioned_lock_holder.h"
#include "chrome/browser/web_applications/locks/partitioned_lock_id.h"
#include "chrome/browser/web_applications/locks/partitioned_lock_manager.h"
#include "chrome/browser/web_applications/locks/shared_web_contents_lock.h"
#include "chrome/browser/web_applications/locks/shared_web_contents_with_app_lock.h"
#include "chrome/browser/web_applications/web_app_command_manager.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "components/webapps/common/web_app_id.h"

namespace web_app {

namespace {

// The WebAppLockManager uses the following breakdown to define its locks with
// the PartitionedLockManager:
// - NoopLock:
//   - No locks.
// - SharedWebContentsLock:
//   - Exclusive {kStatic, kBackgroundWebContents}
// - AllAppsLock:
//   - Exclusive {kStatic, kAllApps}
// - AppLock:
//   - Shared {kStatic, kAllApps}
//   - Exclusive {kApp, <app_id>}
//   - <repeat for each app id requested>
// - SharedWebContentsWithAppLock
//   - Exclusive {kStatic, kBackgroundWebContents}
//   - Shared {kStatic, kAllApps}
//   - Exclusive {kApp, <app_id>}
//   - <repeat for each app id requested>
//
// We use strict lock ordering to ensure no deadlocks occur, and to allow
// 'upgrading'.  Locks must be in this order to facilitate the upgrade
// functionality provided by the system and not deadlock
// - kBackgroundWebContents (static partition)
// - kAllApps (static partition)
// - kApp (app partition)
//
// For example, the lock used for the SharedWebContentsLock  has to be
// 'before' the locks used for the AppLock, as this allows the lock upgrade
// method `UpgradeAndAcquireLock` below where a SharedWebContentsLock can be
// upgraded to a SharedWebContentsWithAppLock.

// These values are not persisted to disk or logs.
enum class LockPartition {
  kStatic = 0,
  kApp = 1,
  kMaxValue = kApp,
};

// These values are not persisted to disk or logs.
enum KeysOnStaticPartition {
  kBackgroundWebContents = 0,
  kAllApps = 1,
  kMaxValue = kAllApps,
};

static_assert(LockPartition::kStatic < LockPartition::kApp);
static_assert(KeysOnStaticPartition::kBackgroundWebContents <
              KeysOnStaticPartition::kAllApps);
// Since we use `base::NumberToString` for the static partition data below, and
// the PartitionedLockId uses alphabetical sorting for the data field, the value
// must stay below 10 until there is a different sorting / encoding scheme.
static_assert(KeysOnStaticPartition::kMaxValue < 10);

const char* KeysOnStaticPartitionToString(KeysOnStaticPartition key) {
  switch (key) {
    case KeysOnStaticPartition::kAllApps:
      return "AllApps";
    case KeysOnStaticPartition::kBackgroundWebContents:
      return "BackgroundWebContents";
  }
}

PartitionedLockId GetAllAppsLockId() {
  return PartitionedLockId(
      {static_cast<int>(LockPartition::kStatic),
       base::NumberToString(KeysOnStaticPartition::kAllApps)});
}

PartitionedLockManager::PartitionedLockRequest GetAllAppsLock(
    PartitionedLockManager::LockType type) {
  return PartitionedLockManager::PartitionedLockRequest(GetAllAppsLockId(),
                                                        type);
}

PartitionedLockManager::PartitionedLockRequest GetSharedWebContentsLock() {
  return PartitionedLockManager::PartitionedLockRequest(
      PartitionedLockId({static_cast<int>(LockPartition::kStatic),
                         base::NumberToString(
                             KeysOnStaticPartition::kBackgroundWebContents)}),
      PartitionedLockManager::LockType::kExclusive);
}

std::vector<PartitionedLockManager::PartitionedLockRequest>
GetExclusiveAppIdLocks(const base::flat_set<webapps::AppId>& app_ids) {
  std::vector<PartitionedLockManager::PartitionedLockRequest> lock_requests;
  for (const webapps::AppId& app_id : app_ids) {
    lock_requests.emplace_back(
        PartitionedLockId({static_cast<int>(LockPartition::kApp), app_id}),
        PartitionedLockManager::LockType::kExclusive);
  }
  return lock_requests;
}

std::vector<PartitionedLockManager::PartitionedLockRequest>
GetLockRequestsForLock(const LockDescription& lock) {
  std::vector<PartitionedLockManager::PartitionedLockRequest> requests;
  switch (lock.type()) {
    case LockDescription::Type::kNoOp:
      return {};
    case LockDescription::Type::kApp:
      requests = GetExclusiveAppIdLocks(lock.app_ids());
      requests.push_back(
          GetAllAppsLock(PartitionedLockManager::LockType::kShared));
      return requests;
    case LockDescription::Type::kAllAppsLock:
      return {GetAllAppsLock(PartitionedLockManager::LockType::kExclusive)};
    case LockDescription::Type::kAppAndWebContents:
      requests = GetExclusiveAppIdLocks(lock.app_ids());
      requests.push_back(
          GetAllAppsLock(PartitionedLockManager::LockType::kShared));
      requests.push_back(GetSharedWebContentsLock());
      return requests;
    case LockDescription::Type::kBackgroundWebContents:
      return {GetSharedWebContentsLock()};
  }
}

#if DCHECK_IS_ON()
void LogLockRequest(
    const LockDescription& description,
    const base::Location& location,
    const std::vector<PartitionedLockManager::PartitionedLockRequest>& requests,
    const PartitionedLockManager& lock_manager) {
  DVLOG(1) << "Requesting or upgrading to lock " << base::ToString(description)
           << " for location " << location.ToString();
  std::vector<base::Location> locations =
      lock_manager.GetHeldAndQueuedLockLocations(requests);
  if (!locations.empty()) {
    DVLOG(1) << "Lock currently held (or queued to be held) by:";
    for (const auto& held_location : locations) {
      DVLOG(1) << " " << held_location.ToString();
    }
  }
}
#endif

}  // namespace

template <class LockType>
void WebAppLockManager::GrantLockWithCallback(
    base::WeakPtr<LockType> lock,
    base::OnceClosure on_lock_acquired) {
  // This callback is never called if the lock holder is destroyed, see
  // invariant on PartitionedLockManager::AcquireLocks
  CHECK(lock);
  lock->GrantLock(*this);
  std::move(on_lock_acquired).Run();
}

template void WebAppLockManager::GrantLockWithCallback<NoopLock>(
    base::WeakPtr<NoopLock> lock,
    base::OnceClosure on_lock_acquired);
template void WebAppLockManager::GrantLockWithCallback<AppLock>(
    base::WeakPtr<AppLock> lock,
    base::OnceClosure on_lock_acquired);
template void WebAppLockManager::GrantLockWithCallback<AllAppsLock>(
    base::WeakPtr<AllAppsLock> lock,
    base::OnceClosure on_lock_acquired);

template <>
void WebAppLockManager::GrantLockWithCallback(
    base::WeakPtr<SharedWebContentsLock> lock,
    base::OnceClosure on_lock_acquired) {
  CHECK(lock);

  // Don't grant lock during profile shutdown to prevent WebContents creation.
  // This prevents the race condition where a lock is granted after
  // BrowserContext shutdown has started, which would cause a CHECK failure
  // in RenderProcessHostImpl constructor.
  if (provider_->profile()->ShutdownStarted()) {
    // CRITICAL: Don't call on_lock_acquired callback during shutdown.
    // Lock is granted by PartitionedLockManager but resources not allocated.
    LOG(WARNING) << "Lock acquisition during shutdown - callback not called";
    return;
  }

  lock->GrantLock(
      *this, *provider_->command_manager().EnsureWebContentsCreated(PassKey()));
  std::move(on_lock_acquired).Run();
}

template <>
void WebAppLockManager::GrantLockWithCallback(
    base::WeakPtr<SharedWebContentsWithAppLock> lock,
    base::OnceClosure on_lock_acquired) {
  CHECK(lock);

  // Don't grant lock during profile shutdown to prevent WebContents creation.
  // This prevents the race condition where a lock is granted after
  // BrowserContext shutdown has started, which would cause a CHECK failure
  // in RenderProcessHostImpl constructor.
  if (provider_->profile()->ShutdownStarted()) {
    // CRITICAL: Don't call on_lock_acquired callback during shutdown.
    // Lock is granted by PartitionedLockManager but resources not allocated.
    LOG(WARNING) << "Lock acquisition during shutdown - callback not called";
    return;
  }

  lock->GrantLock(
      *this, *provider_->command_manager().EnsureWebContentsCreated(PassKey()));
  std::move(on_lock_acquired).Run();
}

WebAppLockManager::WebAppLockManager() = default;
WebAppLockManager::~WebAppLockManager() = default;

void WebAppLockManager::SetProvider(base::PassKey<WebAppCommandManager>,
                                    WebAppProvider& provider) {
  provider_ = &provider;
}

bool WebAppLockManager::IsSharedWebContentsLockFree() {
  return lock_manager_.TestLock(GetSharedWebContentsLock()) ==
         PartitionedLockManager::TestLockResult::kFree;
}

template <class LockType>
void WebAppLockManager::AcquireLock(
    const LockType::LockDescription& lock_description,
    LockType& lock,
    base::OnceClosure on_lock_acquired,
    const base::Location& location) {
  CHECK(!lock.IsGranted());
  PartitionedLockHolder& holder =
      lock.InitializeLockHolderForAcquire(PassKey());
  AcquireLockImpl(
      holder, lock_description,
      base::BindOnce(&WebAppLockManager::GrantLockWithCallback<LockType>,
                     GetWeakPtr(), lock.AsWeakPtr(),
                     std::move(on_lock_acquired)),
      location);
}

template void WebAppLockManager::AcquireLock<NoopLock>(
    const NoopLockDescription& lock_description,
    NoopLock& lock,
    base::OnceClosure on_lock_acquired,
    const base::Location& location);
template void WebAppLockManager::AcquireLock<SharedWebContentsLock>(
    const SharedWebContentsLockDescription& lock_description,
    SharedWebContentsLock& lock,
    base::OnceClosure on_lock_acquired,
    const base::Location& location);
template void WebAppLockManager::AcquireLock<AllAppsLock>(
    const AllAppsLockDescription& lock_description,
    AllAppsLock& lock,
    base::OnceClosure on_lock_acquired,
    const base::Location& location);
template void WebAppLockManager::AcquireLock<AppLock>(
    const AppLockDescription& lock_description,
    AppLock& lock,
    base::OnceClosure on_lock_acquired,
    const base::Location& location);
template void WebAppLockManager::AcquireLock<SharedWebContentsWithAppLock>(
    const SharedWebContentsWithAppLockDescription& lock_description,
    SharedWebContentsWithAppLock& lock,
    base::OnceClosure on_lock_acquired,
    const base::Location& location);

std::unique_ptr<SharedWebContentsWithAppLockDescription>
WebAppLockManager::UpgradeAndAcquireLock(
    std::unique_ptr<SharedWebContentsLock> old_lock,
    SharedWebContentsWithAppLock& new_lock,
    const base::flat_set<webapps::AppId>& app_ids,
    base::OnceClosure on_lock_acquired,
    const base::Location& location) {
  CHECK(!new_lock.IsGranted());
  std::unique_ptr<SharedWebContentsWithAppLockDescription>
      result_lock_description =
          std::make_unique<SharedWebContentsWithAppLockDescription>(app_ids);

  // Note that the description given to `AcquireLock` is the
  // `AppLockDescription` and not the `SharedWebContentsWithAppLock`. This is
  // because `SharedWebContentsLock` already has the web contents lock granted,
  // and we only need the extra app locks.
  PartitionedLockHolder& holder =
      new_lock.InitializeLockHolderForUpgrade(std::move(old_lock), PassKey());
  AcquireLockImpl(holder, AppLockDescription(app_ids),
                  base::BindOnce(&WebAppLockManager::GrantLockWithCallback<
                                     SharedWebContentsWithAppLock>,
                                 GetWeakPtr(), new_lock.AsWeakPtr(),
                                 std::move(on_lock_acquired)),
                  location);

  return result_lock_description;
}

std::unique_ptr<AppLockDescription> WebAppLockManager::UpgradeAndAcquireLock(
    std::unique_ptr<NoopLock> old_lock,
    AppLock& new_lock,
    const base::flat_set<webapps::AppId>& app_ids,
    base::OnceClosure on_lock_acquired,
    const base::Location& location) {
  CHECK(!new_lock.IsGranted());
  std::unique_ptr<AppLockDescription> result_lock_description =
      std::make_unique<AppLockDescription>(app_ids);

  PartitionedLockHolder& holder =
      new_lock.InitializeLockHolderForUpgrade(std::move(old_lock), PassKey());
  AcquireLockImpl(
      holder, *result_lock_description,
      base::BindOnce(&WebAppLockManager::GrantLockWithCallback<AppLock>,
                     GetWeakPtr(), new_lock.AsWeakPtr(),
                     std::move(on_lock_acquired)),
      location);
  return result_lock_description;
}

std::unique_ptr<AllAppsLockDescription>
WebAppLockManager::UpgradeAndAcquireLock(
    std::unique_ptr<SharedWebContentsWithAppLock> old_lock,
    AllAppsLock& new_lock,
    base::OnceClosure on_lock_acquired,
    const base::Location& location) {
  CHECK(!new_lock.IsGranted());
  std::unique_ptr<AllAppsLockDescription> result_lock_description =
      std::make_unique<AllAppsLockDescription>();

  // Upgrading requires the new lock to still hold all of the old locks.
  PartitionedLockHolder& holder =
      new_lock.InitializeLockHolderForUpgrade(std::move(old_lock), PassKey());

  // Upgrade to exclusive.
  lock_manager_.UpgradeToExclusive(
      holder, GetAllAppsLockId(),
      base::BindOnce(&WebAppLockManager::GrantLockWithCallback<AllAppsLock>,
                     GetWeakPtr(), new_lock.AsWeakPtr(),
                     std::move(on_lock_acquired)),
      location);
  return result_lock_description;
}

base::Value WebAppLockManager::ToDebugValue() const {
  return lock_manager_.ToDebugValue([](const PartitionedLockId& lock)
                                        -> std::string {
    // Out of bounds things should NOT happen here, but given this is being
    // called from debug UI, it's better to return something reasonable
    // instead of doing undefined behavior.
    DCHECK_GE(lock.partition, 0);
    DCHECK_LE(lock.partition, static_cast<int>(LockPartition::kMaxValue));
    if (lock.partition < 0 ||
        lock.partition > static_cast<int>(LockPartition::kMaxValue)) {
      return base::StringPrintf("Invalid lock partition: %i", lock.partition);
    }
    LockPartition partition = static_cast<LockPartition>(lock.partition);
    switch (partition) {
      case LockPartition::kApp:
        return base::StrCat({"App, ", lock.key});
      case LockPartition::kStatic: {
        int lock_key = -1;
        if (!base::StringToInt(lock.key, &lock_key)) {
          return base::StringPrintf("Static, invalid number '%s'",
                                    lock.key.c_str());
        }
        DCHECK_GE(lock_key, 0);
        DCHECK_LE(lock_key, static_cast<int>(KeysOnStaticPartition::kMaxValue));
        if (lock_key < 0 ||
            lock_key > static_cast<int>(KeysOnStaticPartition::kMaxValue)) {
          return base::StringPrintf("Static, invalid key number: %i", lock_key);
        }
        KeysOnStaticPartition key =
            static_cast<KeysOnStaticPartition>(lock_key);
        return base::StringPrintf("Static, '%s'",
                                  KeysOnStaticPartitionToString(key));
      }
    }
  });
}

base::WeakPtr<WebAppLockManager> WebAppLockManager::GetWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

void WebAppLockManager::AcquireLockImpl(PartitionedLockHolder& holder,
                                        const LockDescription& lock_description,
                                        base::OnceClosure on_lock_acquired,
                                        const base::Location& location) {
  std::vector<PartitionedLockManager::PartitionedLockRequest> requests =
      GetLockRequestsForLock(lock_description);
#if DCHECK_IS_ON()
  LogLockRequest(lock_description, location, requests, lock_manager_);
#endif
  lock_manager_.AcquireLocks(std::move(requests), holder,
                             std::move(on_lock_acquired), location);
}

}  // namespace web_app
