// 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/download/internal/common/download_db_cache.h"

#include "base/bind.h"
#include "components/download/database/download_db.h"
#include "components/download/database/download_db_conversions.h"
#include "components/download/database/download_db_entry.h"
#include "components/download/public/common/download_features.h"
#include "components/download/public/common/download_stats.h"
#include "components/download/public/common/download_utils.h"

namespace download {

namespace {

// Interval between download DB updates.
// TODO(qinmin): make this finch configurable.
const int kUpdateDBIntervalMs = 10000;

enum class ShouldUpdateDownloadDBResult {
  NO_UPDATE,
  UPDATE,
  UPDATE_IMMEDIATELY,
};

ShouldUpdateDownloadDBResult ShouldUpdateDownloadDB(
    base::Optional<DownloadDBEntry> previous,
    const DownloadDBEntry& current) {
  if (!previous)
    return ShouldUpdateDownloadDBResult::UPDATE_IMMEDIATELY;

  base::Optional<InProgressInfo> previous_info;
  if (previous->download_info)
    previous_info = previous->download_info->in_progress_info;
  base::FilePath previous_path =
      previous_info ? previous_info->current_path : base::FilePath();
  bool previous_paused = previous_info ? previous_info->paused : false;

  base::Optional<InProgressInfo> current_info;
  if (current.download_info)
    current_info = current.download_info->in_progress_info;

  base::FilePath current_path;
  bool paused = false;
  GURL url;
  DownloadItem::DownloadState state = DownloadItem::DownloadState::IN_PROGRESS;
  DownloadInterruptReason interrupt_reason = DOWNLOAD_INTERRUPT_REASON_NONE;
  if (current_info) {
    base::FilePath current_path = current_info->current_path;
    paused = current_info->paused;
    if (!current_info->url_chain.empty())
      url = current_info->url_chain.back();
    state = current_info->state;
    interrupt_reason = current_info->interrupt_reason;
  }

  // When download path is determined, Chrome should commit the history
  // immediately. Otherwise the file will be left permanently on the external
  // storage if Chrome crashes right away.
  if (current_path != previous_path || paused != previous_paused)
    return ShouldUpdateDownloadDBResult::UPDATE_IMMEDIATELY;

  if (previous.value() == current)
    return ShouldUpdateDownloadDBResult::NO_UPDATE;

  if (IsDownloadDone(url, state, interrupt_reason))
    return ShouldUpdateDownloadDBResult::UPDATE_IMMEDIATELY;

  return ShouldUpdateDownloadDBResult::UPDATE;
}

void CleanUpInProgressEntry(DownloadDBEntry* entry) {
  if (!entry->download_info)
    return;

  base::Optional<InProgressInfo>& in_progress_info =
      entry->download_info->in_progress_info;
  if (!in_progress_info)
    return;

  if (in_progress_info->state == DownloadItem::DownloadState::IN_PROGRESS) {
    in_progress_info->state = DownloadItem::DownloadState::INTERRUPTED;
    in_progress_info->interrupt_reason =
        download::DOWNLOAD_INTERRUPT_REASON_CRASH;
    // We should not trust the hash value for crashed in-progress download, as
    // hash is not calculated for when download is in progress.
    in_progress_info->hash = std::string();
  }
}

void OnDownloadDBUpdated(bool success) {
  // TODO(qinmin): handle the case that update fails.
  if (!success)
    LOG(ERROR) << "Unable to update DB entries";
}

// Check if a DownloadDBEntry represents an in progress download.
bool IsInProgressEntry(base::Optional<DownloadDBEntry> entry) {
  if (!entry || !entry->download_info ||
      !entry->download_info->in_progress_info)
    return false;

  return entry->download_info->in_progress_info->state ==
         DownloadItem::DownloadState::IN_PROGRESS;
}

}  // namespace

DownloadDBCache::DownloadDBCache(std::unique_ptr<DownloadDB> download_db)
    : initialized_(false), download_db_(std::move(download_db)) {
  DCHECK(download_db_);
}

DownloadDBCache::~DownloadDBCache() = default;

void DownloadDBCache::Initialize(InitializeCallback callback) {
  DCHECK(!initialized_);
  download_db_->Initialize(
      base::BindOnce(&DownloadDBCache::OnDownloadDBInitialized,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

base::Optional<DownloadDBEntry> DownloadDBCache::RetrieveEntry(
    const std::string& guid) {
  auto iter = cached_entries_.find(guid);
  if (iter != cached_entries_.end())
    return iter->second;
  return base::nullopt;
}

void DownloadDBCache::AddOrReplaceEntry(const DownloadDBEntry& entry) {
  if (!entry.download_info)
    return;
  const std::string& guid = entry.download_info->guid;
  ShouldUpdateDownloadDBResult result =
      ShouldUpdateDownloadDB(RetrieveEntry(guid), entry);
  if (result == ShouldUpdateDownloadDBResult::NO_UPDATE)
    return;
  if (!update_timer_.IsRunning() &&
      result == ShouldUpdateDownloadDBResult::UPDATE) {
    update_timer_.Start(FROM_HERE,
                        base::TimeDelta::FromMilliseconds(kUpdateDBIntervalMs),
                        this, &DownloadDBCache::UpdateDownloadDB);
  }

  cached_entries_[guid] = entry;
  updated_guids_.emplace(guid);
  if (result == ShouldUpdateDownloadDBResult::UPDATE_IMMEDIATELY) {
    UpdateDownloadDB();
    update_timer_.Stop();
  }
}

void DownloadDBCache::RemoveEntry(const std::string& guid) {
  cached_entries_.erase(guid);
  updated_guids_.erase(guid);
  if (initialized_)
    download_db_->Remove(guid);
}

void DownloadDBCache::UpdateDownloadDB() {
  if (updated_guids_.empty())
    return;

  std::vector<DownloadDBEntry> entries;
  for (const auto& guid : updated_guids_) {
    base::Optional<DownloadDBEntry> entry = RetrieveEntry(guid);
    DCHECK(entry);
    entries.emplace_back(entry.value());
    // If the entry is no longer in-progress, remove it from the cache as it may
    // not update again soon.
    if (!IsInProgressEntry(entry))
      cached_entries_.erase(guid);
  }
  updated_guids_.clear();
  if (initialized_) {
    download_db_->AddOrReplaceEntries(entries,
                                      base::BindOnce(&OnDownloadDBUpdated));
  }
}

void DownloadDBCache::OnDownloadUpdated(DownloadItem* download) {
  DownloadDBEntry entry = CreateDownloadDBEntryFromItem(
      *(static_cast<DownloadItemImpl*>(download)));
  AddOrReplaceEntry(entry);
}

void DownloadDBCache::OnDownloadRemoved(DownloadItem* download) {
  RemoveEntry(download->GetGuid());
}

void DownloadDBCache::OnDownloadDBInitialized(
    InitializeCallback callback,
    bool success) {
  if (success) {
    RecordInProgressDBCount(kInitializationSucceededCount);
    download_db_->LoadEntries(
        base::BindOnce(&DownloadDBCache::OnDownloadDBEntriesLoaded,
                       weak_factory_.GetWeakPtr(), std::move(callback)));
  } else {
    RecordInProgressDBCount(kInitializationFailedCount);
    std::move(callback).Run(false,
                            std::make_unique<std::vector<DownloadDBEntry>>());
  }
}

void DownloadDBCache::OnDownloadDBEntriesLoaded(
    InitializeCallback callback,
    bool success,
    std::unique_ptr<std::vector<DownloadDBEntry>> entries) {
  initialized_ = success;
  RecordInProgressDBCount(success ? kLoadSucceededCount : kLoadFailedCount);
  for (auto& entry : *entries) {
    // If the entry is from the metadata cache migration, just remove it from
    // DB as the data is not being cleaned up properly.
    if (entry.download_info->id < 0)
      RemoveEntry(entry.download_info->guid);
    else
      CleanUpInProgressEntry(&entry);
  }
  std::move(callback).Run(success, std::move(entries));
}

void DownloadDBCache::SetTimerTaskRunnerForTesting(
    scoped_refptr<base::SequencedTaskRunner> task_runner) {
  update_timer_.SetTaskRunner(task_runner);
}

}  //  namespace download
