// Copyright 2017 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.

#include "chrome/common/notifications/notification_image_retainer.h"

#include <algorithm>
#include <set>

#include "base/bind.h"
#include "base/containers/contains.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_util.h"
#include "base/memory/ref_counted_memory.h"
#include "base/numerics/safe_conversions.h"
#include "base/path_service.h"
#include "base/task/post_task.h"
#include "base/task/thread_pool.h"
#include "base/time/default_tick_clock.h"
#include "chrome/common/chrome_paths.h"
#include "ui/gfx/image/image.h"

namespace {

constexpr base::FilePath::CharType kImageRoot[] =
    FILE_PATH_LITERAL("Notification Resources");

// How long to keep the temp files before deleting them. The formula for picking
// the delay is t * (n + 1), where t is the default on-screen display time for
// an Action Center notification (6 seconds) and n is the number of
// notifications that can be shown on-screen at once (1).
constexpr base::TimeDelta kDeletionDelay = base::Seconds(12);

// Returns the temporary directory within the user data directory. The regular
// temporary directory is not used to minimize the risk of files getting deleted
// by accident. It is also not profile-bound because the notification bridge
// is profile-agnostic.
base::FilePath DetermineImageDirectory() {
  base::FilePath data_dir;
  bool success = base::PathService::Get(chrome::DIR_USER_DATA, &data_dir);
  DCHECK(success);
  return data_dir.Append(kImageRoot);
}

// Returns the full paths to all immediate file and directory children of |dir|,
// excluding those present in |registered_names|.
std::vector<base::FilePath> GetFilesFromPrevSessions(
    const base::FilePath& dir,
    const std::set<base::FilePath>& registered_names) {
  // |dir| may have sub-dirs, created by the old implementation.
  base::FileEnumerator file_enumerator(
      dir, /*recursive=*/false,
      base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES,
      FILE_PATH_LITERAL("*"));
  std::vector<base::FilePath> files;

  for (base::FilePath current = file_enumerator.Next(); !current.empty();
       current = file_enumerator.Next()) {
    // Exclude any new file created in this session.
    if (!base::Contains(registered_names, current.BaseName()))
      files.push_back(std::move(current));
  }

  return files;
}

// Deletes files in |paths|.
void DeleteFiles(std::vector<base::FilePath> paths) {
  // |file_path| can be a directory, created by the old implementation, so
  // delete it recursively.
  for (const auto& file_path : paths)
    base::DeletePathRecursively(file_path);
}

}  // namespace

NotificationImageRetainer::NotificationImageRetainer(
    scoped_refptr<base::SequencedTaskRunner> deletion_task_runner,
    const base::TickClock* tick_clock)
    : deletion_task_runner_(std::move(deletion_task_runner)),
      image_dir_(DetermineImageDirectory()),
      tick_clock_(tick_clock),
      deletion_timer_(tick_clock) {
  DCHECK(deletion_task_runner_);
  DCHECK(tick_clock);

  DETACH_FROM_SEQUENCE(sequence_checker_);
}

NotificationImageRetainer::NotificationImageRetainer()
    : NotificationImageRetainer(
          base::ThreadPool::CreateSequencedTaskRunner(
              {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
               base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}),
          base::DefaultTickClock::GetInstance()) {}

NotificationImageRetainer::~NotificationImageRetainer() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}

void NotificationImageRetainer::CleanupFilesFromPrevSessions() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // Store all file names from registered_images in an ordered set for quick
  // search.
  std::set<base::FilePath> registered_names;
  for (const auto& pair : registered_images_)
    registered_names.insert(pair.first);

  std::vector<base::FilePath> files =
      GetFilesFromPrevSessions(image_dir_, registered_names);

  // This method is run in an "after startup" task, so it is fine to directly
  // post the DeleteFiles task to the runner.
  if (!files.empty()) {
    deletion_task_runner_->PostTask(
        FROM_HERE, base::BindOnce(&DeleteFiles, std::move(files)));
  }
}

base::FilePath NotificationImageRetainer::RegisterTemporaryImage(
    const gfx::Image& image) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  scoped_refptr<base::RefCountedMemory> data = image.As1xPNGBytes();
  if (data->size() == 0)
    return base::FilePath();

  // Create the image directory. Since Chrome doesn't delete this directory
  // after showing notifications, this directory creation should happen exactly
  // once until Chrome is re-installed.
  if (!base::CreateDirectory(image_dir_))
    return base::FilePath();

  base::FilePath temp_file;
  if (!base::CreateTemporaryFileInDir(image_dir_, &temp_file))
    return base::FilePath();

  const base::TimeTicks now = tick_clock_->NowTicks();
  DCHECK(registered_images_.empty() || now >= registered_images_.back().second);
  registered_images_.emplace_back(temp_file.BaseName(), now);

  // At this point, a temp file is already created. We need to clean it up even
  // if it fails to write the image data to this file.
  int data_len = base::checked_cast<int>(data->size());
  bool data_write_success = (base::WriteFile(temp_file, data->front_as<char>(),
                                             data_len) == data_len);

  // Start the timer if it hasn't to delete the expired files in batch. This
  // avoids creating a deletion task for each file, otherwise the overhead can
  // be large when there is a steady stream of notifications coming rapidly.
  if (!deletion_timer_.IsRunning()) {
    deletion_timer_.Start(
        FROM_HERE, kDeletionDelay,
        base::BindRepeating(&NotificationImageRetainer::DeleteExpiredFiles,
                            weak_ptr_factory_.GetWeakPtr()));
  }

  return data_write_success ? temp_file : base::FilePath();
}

base::OnceClosure NotificationImageRetainer::GetCleanupTask() {
  return base::BindOnce(
      &NotificationImageRetainer::CleanupFilesFromPrevSessions,
      weak_ptr_factory_.GetWeakPtr());
}

void NotificationImageRetainer::DeleteExpiredFiles() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DCHECK(!registered_images_.empty());

  // Find the first file that should not be deleted.
  const base::TimeTicks then = tick_clock_->NowTicks() - kDeletionDelay;
  const auto end =
      std::upper_bound(registered_images_.begin(), registered_images_.end(),
                       std::make_pair(base::FilePath(), then),
                       [](const NameAndTime& a, const NameAndTime& b) {
                         return a.second < b.second;
                       });
  if (end == registered_images_.begin())
    return;  // Nothing to delete yet.

  // Ship the files to be deleted off to the deletion task runner.
  std::vector<base::FilePath> files_to_delete;
  files_to_delete.reserve(end - registered_images_.begin());
  for (auto iter = registered_images_.begin(); iter < end; ++iter)
    files_to_delete.push_back(image_dir_.Append(iter->first));

  deletion_task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&DeleteFiles, std::move(files_to_delete)));

  // Erase the items to be deleted from registered_images_.
  registered_images_.erase(registered_images_.begin(), end);

  // Stop the recurring timer if all files have been deleted.
  if (registered_images_.empty())
    deletion_timer_.Stop();
}