// 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/renderer/loader/sync_load_context.h"

#include <string>

#include "base/logging.h"
#include "base/optional.h"
#include "base/synchronization/waitable_event.h"
#include "base/time/time.h"
#include "content/public/common/url_loader_throttle.h"
#include "content/renderer/loader/navigation_response_override_parameters.h"
#include "content/renderer/loader/sync_load_response.h"
#include "net/url_request/redirect_info.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/resource_response_info.h"

namespace content {

// An inner helper class to manage the SyncLoadContext's events and timeouts,
// so that we can stop or resumse all of them at once.
class SyncLoadContext::SignalHelper final {
 public:
  SignalHelper(SyncLoadContext* context,
               base::WaitableEvent* redirect_or_response_event,
               base::WaitableEvent* abort_event,
               base::TimeDelta timeout)
      : context_(context),
        redirect_or_response_event_(redirect_or_response_event),
        abort_event_(abort_event) {
    // base::TimeDelta::Max() means no timeout.
    if (timeout != base::TimeDelta::Max()) {
      // Instantiate a base::OneShotTimer instance.
      timeout_timer_.emplace();
    }
    Start(timeout);
  }

  void SignalRedirectOrResponseComplete() {
    abort_watcher_.StopWatching();
    if (timeout_timer_)
      timeout_timer_->AbandonAndStop();
    redirect_or_response_event_->Signal();
  }

  bool RestartAfterRedirect() {
    if (abort_event_ && abort_event_->IsSignaled())
      return false;

    base::TimeDelta timeout_remainder = base::TimeDelta::Max();
    if (timeout_timer_) {
      timeout_remainder =
          timeout_timer_->desired_run_time() - base::TimeTicks::Now();
      if (timeout_remainder <= base::TimeDelta())
        return false;
    }
    Start(timeout_remainder);
    return true;
  }

 private:
  void Start(base::TimeDelta timeout) {
    DCHECK(!redirect_or_response_event_->IsSignaled());
    if (abort_event_) {
      abort_watcher_.StartWatching(
          abort_event_,
          base::BindOnce(&SyncLoadContext::OnAbort, base::Unretained(context_)),
          context_->task_runner_);
    }
    if (timeout_timer_) {
      DCHECK_NE(base::TimeDelta::Max(), timeout);
      timeout_timer_->Start(FROM_HERE, timeout, context_,
                            &SyncLoadContext::OnTimeout);
    }
  }

  SyncLoadContext* context_;
  base::WaitableEvent* redirect_or_response_event_;
  base::WaitableEvent* abort_event_;
  base::WaitableEventWatcher abort_watcher_;
  base::Optional<base::OneShotTimer> timeout_timer_;
};

// static
void SyncLoadContext::StartAsyncWithWaitableEvent(
    std::unique_ptr<network::ResourceRequest> request,
    int routing_id,
    scoped_refptr<base::SingleThreadTaskRunner> loading_task_runner,
    const net::NetworkTrafficAnnotationTag& traffic_annotation,
    std::unique_ptr<network::SharedURLLoaderFactoryInfo>
        url_loader_factory_info,
    std::vector<std::unique_ptr<URLLoaderThrottle>> throttles,
    SyncLoadResponse* response,
    base::WaitableEvent* redirect_or_response_event,
    base::WaitableEvent* abort_event,
    base::TimeDelta timeout,
    blink::mojom::BlobRegistryPtrInfo download_to_blob_registry) {
  bool download_to_blob = download_to_blob_registry.is_valid();
  auto* context = new SyncLoadContext(
      request.get(), std::move(url_loader_factory_info), response,
      redirect_or_response_event, abort_event, timeout,
      std::move(download_to_blob_registry), loading_task_runner);
  context->request_id_ = context->resource_dispatcher_->StartAsync(
      std::move(request), routing_id, std::move(loading_task_runner),
      traffic_annotation, true /* is_sync */,
      download_to_blob /* pass_response_pipe_to_peer */,
      base::WrapUnique(context), context->url_loader_factory_,
      std::move(throttles), nullptr /* navigation_response_override_params */,
      nullptr /* continue_for_navigation */);
}

SyncLoadContext::SyncLoadContext(
    network::ResourceRequest* request,
    std::unique_ptr<network::SharedURLLoaderFactoryInfo> url_loader_factory,
    SyncLoadResponse* response,
    base::WaitableEvent* redirect_or_response_event,
    base::WaitableEvent* abort_event,
    base::TimeDelta timeout,
    blink::mojom::BlobRegistryPtrInfo download_to_blob_registry,
    scoped_refptr<base::SingleThreadTaskRunner> task_runner)
    : response_(response),
      download_to_blob_registry_(std::move(download_to_blob_registry)),
      task_runner_(std::move(task_runner)),
      signals_(std::make_unique<SignalHelper>(this,
                                              redirect_or_response_event,
                                              abort_event,
                                              timeout)) {
  url_loader_factory_ =
      network::SharedURLLoaderFactory::Create(std::move(url_loader_factory));

  // Constructs a new ResourceDispatcher specifically for this request.
  resource_dispatcher_ = std::make_unique<ResourceDispatcher>();

  // Initialize the final URL with the original request URL. It will be
  // overwritten on redirects.
  response_->url = request->url;
}

SyncLoadContext::~SyncLoadContext() {}

void SyncLoadContext::OnUploadProgress(uint64_t position, uint64_t size) {}

bool SyncLoadContext::OnReceivedRedirect(
    const net::RedirectInfo& redirect_info,
    const network::ResourceResponseInfo& info) {
  DCHECK(!Completed());
  response_->url = redirect_info.new_url;
  response_->info = info;
  response_->redirect_info = redirect_info;
  response_->context_for_redirect = this;
  resource_dispatcher_->SetDefersLoading(request_id_, true);
  signals_->SignalRedirectOrResponseComplete();
  return true;
}

void SyncLoadContext::FollowRedirect() {
  if (!signals_->RestartAfterRedirect()) {
    CancelRedirect();
    return;
  }

  response_->redirect_info = net::RedirectInfo();
  response_->context_for_redirect = nullptr;

  resource_dispatcher_->SetDefersLoading(request_id_, false);
}

void SyncLoadContext::CancelRedirect() {
  response_->redirect_info = net::RedirectInfo();
  response_->context_for_redirect = nullptr;
  response_->error_code = net::ERR_ABORTED;
  CompleteRequest(true);
}

void SyncLoadContext::OnReceivedResponse(
    const network::ResourceResponseInfo& info) {
  DCHECK(!Completed());
  response_->info = info;
}

void SyncLoadContext::OnStartLoadingResponseBody(
    mojo::ScopedDataPipeConsumerHandle body) {
  DCHECK(download_to_blob_registry_);
  DCHECK(!blob_response_started_);

  blob_response_started_ = true;

  download_to_blob_registry_->RegisterFromStream(
      response_->info.mime_type, "",
      std::max<int64_t>(0, response_->info.content_length), std::move(body),
      nullptr,
      base::BindOnce(&SyncLoadContext::OnFinishCreatingBlob,
                     base::Unretained(this)));
}

void SyncLoadContext::OnReceivedData(std::unique_ptr<ReceivedData> data) {
  DCHECK(!Completed());
  response_->data.append(data->payload(), data->length());
}

void SyncLoadContext::OnTransferSizeUpdated(int transfer_size_diff) {}

void SyncLoadContext::OnCompletedRequest(
    const network::URLLoaderCompletionStatus& status) {
  DCHECK(!Completed());
  response_->error_code = status.error_code;
  response_->extended_error_code = status.extended_error_code;
  response_->cors_error = status.cors_error_status;
  response_->info.encoded_data_length = status.encoded_data_length;
  response_->info.encoded_body_length = status.encoded_body_length;
  if (blob_response_started_ && !blob_finished_) {
    request_completed_ = true;
    return;
  }
  CompleteRequest(true /* remove_pending_request */);
}

void SyncLoadContext::OnFinishCreatingBlob(
    blink::mojom::SerializedBlobPtr blob) {
  DCHECK(!Completed());
  blob_finished_ = true;
  response_->downloaded_blob = std::move(blob);
  if (request_completed_)
    CompleteRequest(true /* remove_pending_request */);
}

void SyncLoadContext::OnAbort(base::WaitableEvent* event) {
  DCHECK(!Completed());
  response_->error_code = net::ERR_ABORTED;
  CompleteRequest(true /* remove_pending_request */);
}

void SyncLoadContext::OnTimeout() {
  // OnTimeout() must not be called after CompleteRequest() was called, because
  // the OneShotTimer must have been stopped.
  DCHECK(!Completed());
  response_->error_code = net::ERR_TIMED_OUT;
  CompleteRequest(true /* remove_pending_request */);
}

void SyncLoadContext::CompleteRequest(bool remove_pending_request) {
  signals_->SignalRedirectOrResponseComplete();
  signals_ = nullptr;
  response_ = nullptr;

  if (remove_pending_request) {
    // This will indirectly cause this object to be deleted.
    resource_dispatcher_->RemovePendingRequest(request_id_, task_runner_);
  }
}

bool SyncLoadContext::Completed() const {
  DCHECK_EQ(!signals_, !response_);
  return !response_;
}

}  // namespace content
