// Copyright 2013 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/ash/extensions/install_limiter.h"

#include <optional>
#include <string>

#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/task/thread_pool.h"
#include "chrome/browser/ash/extensions/install_limiter_factory.h"
#include "content/public/browser/browser_thread.h"
#include "extensions/browser/extensions_browser_client.h"

namespace extensions {

////////////////////////////////////////////////////////////////////////////////
// InstallLimiter::DeferredInstall

InstallLimiter::DeferredInstall::DeferredInstall(
    const scoped_refptr<CrxInstaller>& installer,
    const CRXFileInfo& file_info)
    : installer(installer), file_info(file_info) {}

InstallLimiter::DeferredInstall::DeferredInstall(const DeferredInstall& other) =
    default;

InstallLimiter::DeferredInstall::~DeferredInstall() = default;

////////////////////////////////////////////////////////////////////////////////
// InstallLimiter

// static
InstallLimiter* InstallLimiter::Get(Profile* profile) {
  return InstallLimiterFactory::GetForProfile(profile);
}

// static
bool InstallLimiter::ShouldDeferInstall(int64_t app_size,
                                        const std::string& app_id) {
  constexpr int64_t kBigAppSizeThreshold = 1048576;  // 1MB in bytes
  return app_size > kBigAppSizeThreshold &&
         !ExtensionsBrowserClient::Get()->IsScreensaverInDemoMode(app_id);
}

InstallLimiter::InstallLimiter() : disabled_for_test_(false) {}

InstallLimiter::~InstallLimiter() = default;

void InstallLimiter::DisableForTest() {
  disabled_for_test_ = true;
}

void InstallLimiter::Add(const scoped_refptr<CrxInstaller>& installer,
                         const CRXFileInfo& file_info) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  // No deferred installs when disabled for test.
  if (disabled_for_test_) {
    installer->InstallCrxFile(file_info);
    return;
  }

  num_installs_waiting_for_file_size_++;

  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::MayBlock()}, base::GetFileSizeCallback(file_info.path),
      base::BindOnce(&InstallLimiter::AddWithSize,
                     weak_ptr_factory_.GetWeakPtr(), installer, file_info));
}

void InstallLimiter::OnAllExternalProvidersReady() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  all_external_providers_ready_ = true;

  if (AllInstallsQueuedWithFileSize()) {
    // Stop wait timer and let install notification drive deferred installs.
    wait_timer_.Stop();
    CheckAndRunDeferrredInstalls();
  }
}

void InstallLimiter::AddWithSize(const scoped_refptr<CrxInstaller>& installer,
                                 const CRXFileInfo& file_info,
                                 std::optional<int64_t> size) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);

  num_installs_waiting_for_file_size_--;

  if (!ShouldDeferInstall(size.value_or(0), installer->expected_id()) ||
      AllInstallsQueuedWithFileSize()) {
    RunInstall(installer, file_info);

    // Stop wait timer and let install notification drive deferred installs.
    wait_timer_.Stop();
    return;
  }

  deferred_installs_.push(DeferredInstall(installer, file_info));

  // When there are no running installs, wait a bit before running deferred
  // installs to allow small app install to take precedence, especially when a
  // big app is the first one in the list.
  if (num_running_installs_ == 0 && !wait_timer_.IsRunning()) {
    const int kMaxWaitTimeInMs = 5000;  // 5 seconds.
    wait_timer_.Start(FROM_HERE, base::Milliseconds(kMaxWaitTimeInMs), this,
                      &InstallLimiter::CheckAndRunDeferrredInstalls);
  }
}

void InstallLimiter::CheckAndRunDeferrredInstalls() {
  if (deferred_installs_.empty() || num_running_installs_ > 0)
    return;

  const DeferredInstall& deferred = deferred_installs_.front();
  RunInstall(deferred.installer, deferred.file_info);
  deferred_installs_.pop();
}

void InstallLimiter::RunInstall(const scoped_refptr<CrxInstaller>& installer,
                                const CRXFileInfo& file_info) {
  installer->AddInstallerCallback(base::BindOnce(
      &InstallLimiter::OnInstallerDone, weak_ptr_factory_.GetWeakPtr()));
  installer->InstallCrxFile(file_info);
  num_running_installs_++;
}

void InstallLimiter::OnInstallerDone(
    const std::optional<CrxInstallError>& error) {
  CHECK(num_running_installs_ > 0);
  num_running_installs_--;
  CheckAndRunDeferrredInstalls();
}

bool InstallLimiter::AllInstallsQueuedWithFileSize() const {
  return all_external_providers_ready_ &&
         num_installs_waiting_for_file_size_ == 0;
}

}  // namespace extensions
