// Copyright 2018 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 "components/feed/core/feed_image_database.h"

#include "base/bind.h"
#include "base/logging.h"
#include "base/sequenced_task_runner.h"
#include "base/sys_info.h"
#include "base/task_scheduler/post_task.h"
#include "base/task_scheduler/task_traits.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "components/feed/core/proto/cached_image.pb.h"
#include "components/feed/core/time_serialization.h"
#include "components/leveldb_proto/proto_database_impl.h"

namespace feed {

namespace {
// Statistics are logged to UMA with this string as part of histogram name. They
// can all be found under LevelDB.*.FeedImageDatabase. Changing this needs to
// synchronize with histograms.xml, AND will also become incompatible with older
// browsers still reporting the previous values.
const char kImageDatabaseUMAClientName[] = "FeedImageDatabase";

const char kImageDatabaseFolder[] = "images";

const size_t kDatabaseWriteBufferSizeBytes = 512 * 1024;
const size_t kDatabaseWriteBufferSizeBytesForLowEndDevice = 128 * 1024;
}  // namespace

FeedImageDatabase::FeedImageDatabase(const base::FilePath& database_dir)
    : FeedImageDatabase(
          database_dir,
          std::make_unique<leveldb_proto::ProtoDatabaseImpl<CachedImageProto>>(
              base::CreateSequencedTaskRunnerWithTraits(
                  {base::MayBlock(), base::TaskPriority::BACKGROUND,
                   base::TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN}))) {}

FeedImageDatabase::FeedImageDatabase(
    const base::FilePath& database_dir,
    std::unique_ptr<leveldb_proto::ProtoDatabase<CachedImageProto>>
        image_database)
    : database_status_(UNINITIALIZED),
      image_database_(std::move(image_database)),
      weak_ptr_factory_(this) {
  leveldb_env::Options options = leveldb_proto::CreateSimpleOptions();
  if (base::SysInfo::IsLowEndDevice()) {
    options.write_buffer_size = kDatabaseWriteBufferSizeBytesForLowEndDevice;
  } else {
    options.write_buffer_size = kDatabaseWriteBufferSizeBytes;
  }
  base::FilePath image_dir = database_dir.AppendASCII(kImageDatabaseFolder);
  image_database_->Init(
      kImageDatabaseUMAClientName, image_dir, options,
      base::BindOnce(&FeedImageDatabase::OnDatabaseInitialized,
                     weak_ptr_factory_.GetWeakPtr()));
}

FeedImageDatabase::~FeedImageDatabase() = default;

bool FeedImageDatabase::IsInitialized() {
  return INITIALIZED == database_status_;
}

void FeedImageDatabase::SaveImage(const std::string& url,
                                  const std::string& image_data) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // If database is not ready, ignore the request.
  if (!IsInitialized())
    return;

  CachedImageProto image_proto;
  image_proto.set_url(url);
  image_proto.set_data(image_data);
  image_proto.set_last_used_time(ToDatabaseTime(base::Time::Now()));

  SaveImageImpl(url, image_proto);
}

void FeedImageDatabase::LoadImage(const std::string& url,
                                  FeedImageDatabaseCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  switch (database_status_) {
    case INITIALIZED:
    case INIT_FAILURE:
      LoadImageImpl(url, std::move(callback));
      break;
    case UNINITIALIZED:
      pending_image_callbacks_.emplace_back(url, std::move(callback));
      break;
    default:
      NOTREACHED();
  }
}

void FeedImageDatabase::DeleteImage(const std::string& url) {
  DeleteImageImpl(url, base::BindOnce(&FeedImageDatabase::OnImageUpdated,
                                      weak_ptr_factory_.GetWeakPtr()));
}

void FeedImageDatabase::GarbageCollectImages(
    base::Time expired_time,
    FeedImageDatabaseOperationCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

  // If database is not initialized yet, ignore the request.
  if (!IsInitialized()) {
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), false));
    return;
  }

  image_database_->LoadEntries(base::BindOnce(
      &FeedImageDatabase::GarbageCollectImagesImpl,
      weak_ptr_factory_.GetWeakPtr(), expired_time, std::move(callback)));
}

void FeedImageDatabase::OnDatabaseInitialized(bool success) {
  DCHECK_EQ(database_status_, UNINITIALIZED);

  if (success) {
    database_status_ = INITIALIZED;
  } else {
    database_status_ = INIT_FAILURE;
    DVLOG(1) << "FeedImageDatabase init failed.";
  }

  ProcessPendingImageLoads();
}

void FeedImageDatabase::ProcessPendingImageLoads() {
  DCHECK_NE(database_status_, UNINITIALIZED);

  for (auto& image_callback : pending_image_callbacks_)
    LoadImageImpl(image_callback.first, std::move(image_callback.second));

  pending_image_callbacks_.clear();
}

void FeedImageDatabase::SaveImageImpl(std::string url,
                                      const CachedImageProto& image_proto) {
  auto entries_to_save = std::make_unique<ImageKeyEntryVector>();
  entries_to_save->emplace_back(std::move(url), image_proto);

  image_database_->UpdateEntries(
      std::move(entries_to_save), std::make_unique<std::vector<std::string>>(),
      base::BindOnce(&FeedImageDatabase::OnImageUpdated,
                     weak_ptr_factory_.GetWeakPtr()));
}

void FeedImageDatabase::OnImageLoaded(std::string url,
                                      FeedImageDatabaseCallback callback,
                                      bool success,
                                      std::unique_ptr<CachedImageProto> entry) {
  if (!success || !entry) {
    DVLOG_IF(1, !success) << "FeedImageDatabase load failed.";
    std::move(callback).Run(std::string());
    return;
  }

  DCHECK_EQ(url, entry->url());
  std::move(callback).Run(entry->data());

  // Update timestamp for image.
  entry->set_last_used_time(ToDatabaseTime(base::Time::Now()));
  SaveImageImpl(std::move(url), *entry);
}

void FeedImageDatabase::LoadImageImpl(const std::string& url,
                                      FeedImageDatabaseCallback callback) {
  DCHECK_NE(database_status_, UNINITIALIZED);

  if (IsInitialized()) {
    image_database_->GetEntry(
        url, base::BindOnce(&FeedImageDatabase::OnImageLoaded,
                            weak_ptr_factory_.GetWeakPtr(), url,
                            std::move(callback)));
  } else {
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), std::string()));
  }
}

void FeedImageDatabase::OnImageUpdated(bool success) {
  DVLOG_IF(1, !success) << "FeedImageDatabase update failed.";
}

void FeedImageDatabase::DeleteImageImpl(
    const std::string& url,
    FeedImageDatabaseOperationCallback callback) {
  image_database_->UpdateEntries(
      std::make_unique<ImageKeyEntryVector>(),
      std::make_unique<std::vector<std::string>>(1, url), std::move(callback));
}

void FeedImageDatabase::GarbageCollectImagesImpl(
    base::Time expired_time,
    FeedImageDatabaseOperationCallback callback,
    bool load_entries_success,
    std::unique_ptr<std::vector<CachedImageProto>> image_entries) {
  if (!load_entries_success) {
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(&FeedImageDatabase::OnGarbageCollectionDone,
                                  weak_ptr_factory_.GetWeakPtr(),
                                  std::move(callback), false));
    return;
  }

  int64_t expired_database_time = ToDatabaseTime(expired_time);
  auto keys_to_remove = std::make_unique<std::vector<std::string>>();
  for (const CachedImageProto& image : *image_entries) {
    if (image.last_used_time() < expired_database_time) {
      keys_to_remove->emplace_back(image.url());
    }
  }

  if (keys_to_remove->empty()) {
    base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(std::move(callback), true));
    return;
  }

  image_database_->UpdateEntries(
      std::make_unique<ImageKeyEntryVector>(), std::move(keys_to_remove),
      base::BindOnce(&FeedImageDatabase::OnGarbageCollectionDone,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void FeedImageDatabase::OnGarbageCollectionDone(
    FeedImageDatabaseOperationCallback callback,
    bool success) {
  DVLOG_IF(1, !success) << "FeedImageDatabase garbage collection failed.";
  base::ThreadTaskRunnerHandle::Get()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), success));
}

}  // namespace feed
