// 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 "content/browser/background_fetch/background_fetch_scheduler.h"

#include "base/guid.h"
#include "content/browser/background_fetch/background_fetch_job_controller.h"

namespace content {

BackgroundFetchScheduler::Controller::Controller(
    const BackgroundFetchRegistrationId& registration_id,
    FinishedCallback finished_callback)
    : registration_id_(registration_id),
      finished_callback_(std::move(finished_callback)) {
  DCHECK(finished_callback_);
}

BackgroundFetchScheduler::Controller::~Controller() = default;

void BackgroundFetchScheduler::Controller::Finish(
    BackgroundFetchReasonToAbort reason_to_abort) {
  DCHECK(reason_to_abort != BackgroundFetchReasonToAbort::NONE ||
         !HasMoreRequests());

  std::move(finished_callback_).Run(registration_id_, reason_to_abort);
}

BackgroundFetchScheduler::BackgroundFetchScheduler(
    BackgroundFetchScheduler::RequestProvider* request_provider)
    : request_provider_(request_provider) {}

BackgroundFetchScheduler::~BackgroundFetchScheduler() = default;

void BackgroundFetchScheduler::RemoveJobController(
    const BackgroundFetchRegistrationId& registration_id) {
  for (auto iter = controller_queue_.begin(); iter != controller_queue_.end();
       /* no increment */) {
    if ((**iter).registration_id() == registration_id)
      iter = controller_queue_.erase(iter);
    else
      iter++;
  }
}

void BackgroundFetchScheduler::AddJobController(
    BackgroundFetchScheduler::Controller* controller) {
  controller_queue_.push_back(controller);

  if (!controller_queue_.empty() &&
      download_controller_map_.size() < max_concurrent_downloads_) {
    ScheduleDownload();
  }
}

void BackgroundFetchScheduler::ScheduleDownload() {
  DCHECK(download_controller_map_.size() < max_concurrent_downloads_);

  if (lock_scheduler_ || controller_queue_.empty())
    return;

  auto* controller = controller_queue_.front();
  controller_queue_.pop_front();

  // Making an async call, `ScheduleDownload` shouldn't be called anymore.
  lock_scheduler_ = true;
  request_provider_->PopNextRequest(
      controller->registration_id(),
      base::BindOnce(&BackgroundFetchScheduler::DidPopNextRequest,
                     base::Unretained(this), controller));
}

void BackgroundFetchScheduler::DidPopNextRequest(
    BackgroundFetchScheduler::Controller* controller,
    scoped_refptr<BackgroundFetchRequestInfo> request_info) {
  DCHECK(controller);
  lock_scheduler_ = false;  // Can schedule downloads again.

  // Storage error, fetch might have been aborted.
  if (!request_info) {
    ScheduleDownload();
    return;
  }

  download_controller_map_[request_info->download_guid()] = controller;
  controller->StartRequest(request_info);

  if (download_controller_map_.size() < max_concurrent_downloads_)
    ScheduleDownload();
}

void BackgroundFetchScheduler::MarkRequestAsComplete(
    const BackgroundFetchRegistrationId& registration_id,
    scoped_refptr<BackgroundFetchRequestInfo> request) {
  DCHECK(download_controller_map_.count(request->download_guid()));
  auto* controller = download_controller_map_[request->download_guid()];
  download_controller_map_.erase(request->download_guid());

  request_provider_->MarkRequestAsComplete(
      controller->registration_id(), request.get(),
      base::BindOnce(&BackgroundFetchScheduler::DidMarkRequestAsComplete,
                     base::Unretained(this), controller));
}

void BackgroundFetchScheduler::DidMarkRequestAsComplete(
    BackgroundFetchScheduler::Controller* controller) {
  if (controller->HasMoreRequests())
    controller_queue_.push_back(controller);
  else
    controller->Finish(BackgroundFetchReasonToAbort::NONE);

  ScheduleDownload();
}

void BackgroundFetchScheduler::OnJobAborted(
    const BackgroundFetchRegistrationId& registration_id,
    std::vector<std::string> aborted_guids) {
  // For every active download that was aborted, remove it and schedule a new
  // download.
  for (const auto& guid : aborted_guids) {
    download_controller_map_.erase(guid);
    ScheduleDownload();
  }
}

}  // namespace content
