// Copyright (c) 2019 The Chromium Embedded Framework Authors. Portions
// Copyright (c) 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 "libcef/browser/net_service/browser_urlrequest_impl.h"

#include <string>
#include <utility>

#include "libcef/browser/browser_context.h"
#include "libcef/browser/frame_host_impl.h"
#include "libcef/browser/net_service/url_loader_factory_getter.h"
#include "libcef/browser/request_context_impl.h"
#include "libcef/browser/thread_util.h"
#include "libcef/common/net_service/net_service_util.h"
#include "libcef/common/request_impl.h"
#include "libcef/common/response_impl.h"
#include "libcef/common/task_runner_impl.h"

#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/message_loop/message_loop.h"
#include "base/strings/string_util.h"
#include "content/public/browser/global_request_id.h"
#include "content/public/browser/render_frame_host.h"
#include "net/base/net_errors.h"
#include "net/http/http_response_headers.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/cpp/simple_url_loader_stream_consumer.h"

namespace {

const int32_t kInitialRequestID = -2;

// Request ID for requests initiated by CefBrowserURLRequest. request_ids
// generated by child processes are counted up from 0, while browser
// created requests start at -2 and go down from there. (We need to start at -2
// because -1 is used as a special value all over the resource_dispatcher_host
// for uninitialized variables.) The resource_dispatcher_host code path is not
// used when NetworkService is enabled so it's safe to repurpose the -2 and
// below range here.
// This method is only called on the UI thread.
int32_t MakeRequestID() {
  static int32_t request_id = kInitialRequestID;
  return --request_id;
}

// Manages the mapping of request IDs to request objects.
class RequestManager {
 public:
  RequestManager() {}
  ~RequestManager() { DCHECK(map_.empty()); }

  void Add(int32_t request_id,
           CefRefPtr<CefBrowserURLRequest> request,
           CefRefPtr<CefURLRequestClient> client) {
    DCHECK_LE(request_id, kInitialRequestID);

    base::AutoLock lock_scope(lock_);
    DCHECK(map_.find(request_id) == map_.end());
    map_.insert(std::make_pair(request_id, std::make_pair(request, client)));
  }

  void Remove(int32_t request_id) {
    if (request_id > kInitialRequestID)
      return;

    base::AutoLock lock_scope(lock_);
    RequestMap::iterator it = map_.find(request_id);
    DCHECK(it != map_.end());
    map_.erase(it);
  }

  base::Optional<CefBrowserURLRequest::RequestInfo> Get(int32_t request_id) {
    if (request_id > kInitialRequestID)
      return base::nullopt;

    base::AutoLock lock_scope(lock_);
    RequestMap::const_iterator it = map_.find(request_id);
    if (it != map_.end()) {
      return it->second;
    }
    return base::nullopt;
  }

 private:
  base::Lock lock_;

  using RequestMap = std::map<int32_t, CefBrowserURLRequest::RequestInfo>;
  RequestMap map_;

  DISALLOW_COPY_AND_ASSIGN(RequestManager);
};

#if DCHECK_IS_ON()
// Because of DCHECK()s in the object destructor.
base::LazyInstance<RequestManager>::DestructorAtExit g_manager =
    LAZY_INSTANCE_INITIALIZER;
#else
base::LazyInstance<RequestManager>::Leaky g_manager = LAZY_INSTANCE_INITIALIZER;
#endif

}  // namespace

// CefBrowserURLRequest::Context ----------------------------------------------

class CefBrowserURLRequest::Context
    : public network::SimpleURLLoaderStreamConsumer {
 public:
  Context(CefRefPtr<CefBrowserURLRequest> url_request,
          CefRefPtr<CefFrame> frame,
          CefRefPtr<CefRequest> request,
          CefRefPtr<CefURLRequestClient> client,
          CefRefPtr<CefRequestContext> request_context)
      : url_request_(url_request),
        frame_(frame),
        request_(static_cast<CefRequestImpl*>(request.get())),
        client_(client),
        request_context_(request_context),
        task_runner_(CefTaskRunnerImpl::GetCurrentTaskRunner()),
        response_(new CefResponseImpl()),
        weak_ptr_factory_(this) {
    // Mark the request/response objects as read-only.
    request_->SetReadOnly(true);
    response_->SetReadOnly(true);
  }
  ~Context() override = default;

  bool Start() {
    DCHECK(CalledOnValidThread());

    const GURL& url = GURL(request_->GetURL().ToString());
    if (!url.is_valid())
      return false;

    CEF_POST_TASK(
        CEF_UIT,
        base::BindOnce(
            &CefBrowserURLRequest::Context::GetURLLoaderFactoryGetterOnUIThread,
            frame_, request_context_, weak_ptr_factory_.GetWeakPtr(),
            task_runner_));

    return true;
  }

  void Cancel() {
    DCHECK(CalledOnValidThread());

    // The request may already be complete or canceled.
    if (!url_request_)
      return;

    DCHECK_EQ(status_, UR_IO_PENDING);
    status_ = UR_CANCELED;

    response_->SetReadOnly(false);
    response_->SetError(ERR_ABORTED);
    response_->SetReadOnly(true);

    cleanup_immediately_ = true;
    OnComplete(false);
  }

  CefRefPtr<CefRequest> request() const { return request_.get(); }
  CefRefPtr<CefURLRequestClient> client() const { return client_; }
  CefURLRequest::Status status() const { return status_; }
  CefRefPtr<CefResponse> response() const { return response_.get(); }
  bool response_was_cached() const { return response_was_cached_; }

  inline bool CalledOnValidThread() {
    return task_runner_->RunsTasksInCurrentSequence();
  }

 private:
  static void GetURLLoaderFactoryGetterOnUIThread(
      CefRefPtr<CefFrame> frame,
      CefRefPtr<CefRequestContext> request_context,
      base::WeakPtr<CefBrowserURLRequest::Context> self,
      scoped_refptr<base::SequencedTaskRunner> task_runner) {
    CEF_REQUIRE_UIT();

    // Get or create the request context and browser context.
    CefRefPtr<CefRequestContextImpl> request_context_impl =
        CefRequestContextImpl::GetOrCreateForRequestContext(request_context);
    DCHECK(request_context_impl);
    CefBrowserContext* browser_context =
        request_context_impl->GetBrowserContext();
    DCHECK(browser_context);

    int render_frame_id = MSG_ROUTING_NONE;
    scoped_refptr<net_service::URLLoaderFactoryGetter> loader_factory_getter;
    if (frame) {
      // The request will be associated with this frame/browser if it's valid,
      // otherwise the request will be canceled.
      content::RenderFrameHost* rfh =
          static_cast<CefFrameHostImpl*>(frame.get())->GetRenderFrameHost();
      if (rfh) {
        // In cases where authentication is required this value will be passed
        // as the |routing_id| parameter to
        // NetworkServiceClient::OnAuthRequired. Despite the naming the
        // GetWebContents method in network_service_client.cc expects it to be a
        // FrameTreeNodeId. The |process_id| parameter will always be
        // network::mojom::kBrowserProcessId (value 0) for these requests.
        render_frame_id = rfh->GetFrameTreeNodeId();

        loader_factory_getter =
            net_service::URLLoaderFactoryGetter::Create(rfh, browser_context);
      }
    } else {
      loader_factory_getter =
          net_service::URLLoaderFactoryGetter::Create(nullptr, browser_context);
    }

    task_runner->PostTask(
        FROM_HERE,
        base::BindOnce(
            &CefBrowserURLRequest::Context::ContinueOnOriginatingThread, self,
            render_frame_id, MakeRequestID(), loader_factory_getter));
  }

  void ContinueOnOriginatingThread(
      int render_frame_id,
      int32_t request_id,
      scoped_refptr<net_service::URLLoaderFactoryGetter>
          loader_factory_getter) {
    DCHECK(CalledOnValidThread());

    // The request may have been canceled.
    if (!url_request_)
      return;

    if (!loader_factory_getter) {
      // Cancel the request immediately.
      Cancel();
      return;
    }

    DCHECK_EQ(status_, UR_IO_PENDING);

    loader_factory_getter_ = loader_factory_getter;

    const int request_flags = request_->GetFlags();

    // Create the URLLoaderFactory and bind to this thread.
    auto loader_factory = loader_factory_getter_->GetURLLoaderFactory();

    auto resource_request = std::make_unique<network::ResourceRequest>();
    static_cast<CefRequestImpl*>(request_.get())
        ->Get(resource_request.get(), false);

    resource_request->render_frame_id = render_frame_id;

    // SimpleURLLoader is picky about the body contents. Try to populate them
    // correctly below.
    auto request_body = resource_request->request_body;
    resource_request->request_body = nullptr;

    std::string content_type;
    std::string method = resource_request->method;
    if (request_body) {
      if (method == "GET" || method == "HEAD") {
        // Fix the method value to allow a request body.
        method = "POST";
        resource_request->method = method;

        request_->SetReadOnly(false);
        request_->SetMethod(method);
        request_->SetReadOnly(true);
      }
      resource_request->headers.GetHeader(net::HttpRequestHeaders::kContentType,
                                          &content_type);
    }

    loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
                                               NO_TRAFFIC_ANNOTATION_YET);

    // Associate the request with |request_id|.
    request_id_ = request_id;
    loader_->SetRequestId(request_id);
    g_manager.Get().Add(request_id, url_request_, client_);

    if (request_body) {
      if (request_body->elements()->size() == 1) {
        const auto& element = (*request_body->elements())[0];
        if (element.type() == network::mojom::DataElementType::kFile) {
          if (content_type.empty()) {
            const auto& extension = element.path().Extension();
            if (!extension.empty()) {
              // Requests should not block on the disk! On POSIX this goes to
              // disk. http://code.google.com/p/chromium/issues/detail?id=59849
              base::ThreadRestrictions::ScopedAllowIO allow_io;
              // Also remove the leading period.
              net::GetMimeTypeFromExtension(extension.substr(1), &content_type);
            }
          }
          loader_->AttachFileForUpload(element.path(), content_type);
        } else if (element.type() == network::mojom::DataElementType::kBytes) {
          if (content_type.empty()) {
            content_type = net_service::kContentTypeApplicationFormURLEncoded;
          }
          loader_->AttachStringForUpload(
              std::string(element.bytes() + element.offset(),
                          element.length() - element.offset()),
              content_type);

          if (request_flags & UR_FLAG_REPORT_UPLOAD_PROGRESS) {
            // Report the expected upload data size.
            upload_data_size_ = element.length() - element.offset();
          }
        } else {
          NOTIMPLEMENTED() << "Unsupported element type: " << element.type();
        }
      } else if (request_body->elements()->size() > 1) {
        NOTIMPLEMENTED() << "Multi-part form data is not supported";
      }
    }

    // Allow delivery of non-2xx response bodies.
    loader_->SetAllowHttpErrorResults(true);

    if (!(request_flags & UR_FLAG_NO_RETRY_ON_5XX)) {
      // Allow 2 retries on 5xx response or network change.
      // TODO(network): Consider exposing configuration of max retries and/or
      // RETRY_ON_NETWORK_CHANGE as a separate flag.
      loader_->SetRetryOptions(
          2, network::SimpleURLLoader::RETRY_ON_5XX |
                 network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE);
    }

    if (request_flags & UR_FLAG_STOP_ON_REDIRECT) {
      // The request will be canceled in OnRedirect.
      loader_->SetOnRedirectCallback(
          base::Bind(&CefBrowserURLRequest::Context::OnRedirect,
                     weak_ptr_factory_.GetWeakPtr()));
    }

    if (request_flags & UR_FLAG_REPORT_UPLOAD_PROGRESS) {
      loader_->SetOnUploadProgressCallback(
          base::Bind(&CefBrowserURLRequest::Context::OnUploadProgress,
                     weak_ptr_factory_.GetWeakPtr()));
    }

    if ((request_flags & UR_FLAG_NO_DOWNLOAD_DATA) || method == "HEAD") {
      loader_->DownloadHeadersOnly(
          loader_factory.get(),
          base::BindOnce(&CefBrowserURLRequest::Context::OnHeadersOnly,
                         weak_ptr_factory_.GetWeakPtr()));
    } else {
      loader_->SetOnResponseStartedCallback(
          base::BindOnce(&CefBrowserURLRequest::Context::OnResponseStarted,
                         weak_ptr_factory_.GetWeakPtr()));
      loader_->SetOnDownloadProgressCallback(
          base::Bind(&CefBrowserURLRequest::Context::OnDownloadProgress,
                     weak_ptr_factory_.GetWeakPtr()));

      loader_->DownloadAsStream(loader_factory.get(), this);
    }
  }

  void OnHeadersOnly(scoped_refptr<net::HttpResponseHeaders> headers) {
    DCHECK(CalledOnValidThread());
    DCHECK_EQ(status_, UR_IO_PENDING);

    response_->SetReadOnly(false);
    response_->SetResponseHeaders(*headers);
    response_->SetReadOnly(true);

    // Match the previous behavior of sending download progress notifications
    // for UR_FLAG_NO_DOWNLOAD_DATA requests but not HEAD requests.
    if (request_->GetMethod().ToString() != "HEAD") {
      download_data_size_ = headers->GetContentLength();
      OnDownloadProgress(0);
    }

    cleanup_immediately_ = true;
    OnComplete(true);
  }

  void OnRedirect(const net::RedirectInfo& redirect_info,
                  const network::ResourceResponseHead& response_head,
                  std::vector<std::string>* removed_headers) {
    DCHECK(CalledOnValidThread());
    DCHECK_EQ(status_, UR_IO_PENDING);

    // This method is only called if we intend to stop on redirects.
    DCHECK(request_->GetFlags() | UR_FLAG_STOP_ON_REDIRECT);

    response_->SetReadOnly(false);
    response_->SetURL(redirect_info.new_url.spec());
    response_->SetResponseHeaders(*response_head.headers);
    response_->SetReadOnly(true);

    Cancel();
  }

  void OnResponseStarted(const GURL& final_url,
                         const network::ResourceResponseHead& response_head) {
    DCHECK(CalledOnValidThread());
    DCHECK_EQ(status_, UR_IO_PENDING);

    response_->SetReadOnly(false);
    response_->SetURL(final_url.spec());
    response_->SetResponseHeaders(*response_head.headers);
    response_->SetReadOnly(true);

    download_data_size_ = response_head.content_length;
  }

  void OnUploadProgress(uint64_t position, uint64_t total) {
    DCHECK(CalledOnValidThread());
    DCHECK_EQ(status_, UR_IO_PENDING);

    upload_data_size_ = total;
    if (position == total)
      got_upload_progress_complete_ = true;

    client_->OnUploadProgress(url_request_.get(), position, total);
  }

  void OnDownloadProgress(uint64_t current) {
    DCHECK(CalledOnValidThread());
    DCHECK_EQ(status_, UR_IO_PENDING);

    if (response_->GetStatus() == 0) {
      // With failed requests this callback may arrive without a proceeding
      // OnHeadersOnly or OnResponseStarted.
      return;
    }

    NotifyUploadProgressIfNecessary();

    client_->OnDownloadProgress(url_request_.get(), current,
                                download_data_size_);
  }

  void NotifyUploadProgressIfNecessary() {
    if (!got_upload_progress_complete_ && upload_data_size_ > 0) {
      // URLLoader sends upload notifications using a timer and will not send
      // a notification if the request completes too quickly. We therefore
      // send the notification here if necessary.
      client_->OnUploadProgress(url_request_.get(), upload_data_size_,
                                upload_data_size_);
      got_upload_progress_complete_ = true;
    }
  }

  // SimpleURLLoaderStreamConsumer methods:
  void OnDataReceived(base::StringPiece string_piece,
                      base::OnceClosure resume) override {
    DCHECK(CalledOnValidThread());
    DCHECK_EQ(status_, UR_IO_PENDING);

    client_->OnDownloadData(url_request_.get(), string_piece.data(),
                            string_piece.length());
    std::move(resume).Run();
  }

  void OnComplete(bool success) override {
    DCHECK(CalledOnValidThread());

    // The request may already be complete or canceled.
    if (!url_request_)
      return;

    // Status will be UR_IO_PENDING if we're called when the request is complete
    // (via SimpleURLLoaderStreamConsumer or OnHeadersOnly). We can only call
    // these SimpleURLLoader methods if the request is complete.
    if (status_ == UR_IO_PENDING) {
      status_ = success ? UR_SUCCESS : UR_FAILED;

      response_->SetReadOnly(false);
      response_->SetURL(loader_->GetFinalURL().spec());
      response_->SetError(static_cast<cef_errorcode_t>(loader_->NetError()));
      response_->SetReadOnly(true);

      response_was_cached_ = loader_->LoadedFromCache();
    }

    if (success)
      NotifyUploadProgressIfNecessary();

    client_->OnRequestComplete(url_request_.get());

    // When called via SimpleURLLoaderStreamConsumer we need to cleanup
    // asynchronously. If the load is still pending this will also cancel it.
    Cleanup();
  }

  void OnRetry(base::OnceClosure start_retry) override {
    DCHECK(CalledOnValidThread());
    DCHECK_EQ(status_, UR_IO_PENDING);
    std::move(start_retry).Run();
  }

  void Cleanup() {
    DCHECK(CalledOnValidThread());
    DCHECK(url_request_);

    g_manager.Get().Remove(request_id_);

    client_ = nullptr;
    request_context_ = nullptr;

    // We may be canceled before the loader is created.
    if (loader_) {
      // Must delete the loader before the factory.
      if (cleanup_immediately_) {
        // Most SimpleURLLoader callbacks let us delete the URLLoader objects
        // immediately.
        loader_.reset();
        loader_factory_getter_ = nullptr;
      } else {
        // Delete the URLLoader objects asynchronously on the correct thread.
        task_runner_->DeleteSoon(FROM_HERE, std::move(loader_));
        task_runner_->ReleaseSoon(FROM_HERE, std::move(loader_factory_getter_));
      }
    }

    // We may be holding the last reference to |url_request_|, destruction of
    // which will delete |this|. Use a local variable to keep |url_request_|
    // alive until this method returns.
    auto url_request = url_request_;
    url_request_ = nullptr;
  }

  // Members only accessed on the initialization thread.
  CefRefPtr<CefBrowserURLRequest> url_request_;
  CefRefPtr<CefFrame> frame_;
  CefRefPtr<CefRequestImpl> request_;
  CefRefPtr<CefURLRequestClient> client_;
  CefRefPtr<CefRequestContext> request_context_;
  scoped_refptr<base::SequencedTaskRunner> task_runner_;

  scoped_refptr<net_service::URLLoaderFactoryGetter> loader_factory_getter_;
  std::unique_ptr<network::SimpleURLLoader> loader_;

  int32_t request_id_ = 0;

  CefURLRequest::Status status_ = UR_IO_PENDING;
  CefRefPtr<CefResponseImpl> response_;
  bool response_was_cached_ = false;
  int64 upload_data_size_ = 0;
  int64 download_data_size_ = -1;
  bool got_upload_progress_complete_ = false;
  bool cleanup_immediately_ = false;

  // Must be the last member.
  base::WeakPtrFactory<CefBrowserURLRequest::Context> weak_ptr_factory_;
};

// CefBrowserURLRequest -------------------------------------------------------

// static
base::Optional<CefBrowserURLRequest::RequestInfo>
CefBrowserURLRequest::FromRequestID(int32_t request_id) {
  return g_manager.Get().Get(request_id);
}

// static
base::Optional<CefBrowserURLRequest::RequestInfo>
CefBrowserURLRequest::FromRequestID(
    const content::GlobalRequestID& request_id) {
  if (request_id.child_id == network::mojom::kBrowserProcessId) {
    return FromRequestID(request_id.request_id);
  }
  return base::nullopt;
}

CefBrowserURLRequest::CefBrowserURLRequest(
    CefRefPtr<CefFrame> frame,
    CefRefPtr<CefRequest> request,
    CefRefPtr<CefURLRequestClient> client,
    CefRefPtr<CefRequestContext> request_context) {
  context_.reset(new Context(this, frame, request, client, request_context));
}

CefBrowserURLRequest::~CefBrowserURLRequest() {}

bool CefBrowserURLRequest::Start() {
  if (!VerifyContext())
    return false;
  return context_->Start();
}

CefRefPtr<CefRequest> CefBrowserURLRequest::GetRequest() {
  if (!VerifyContext())
    return NULL;
  return context_->request();
}

CefRefPtr<CefURLRequestClient> CefBrowserURLRequest::GetClient() {
  if (!VerifyContext())
    return NULL;
  return context_->client();
}

CefURLRequest::Status CefBrowserURLRequest::GetRequestStatus() {
  if (!VerifyContext())
    return UR_UNKNOWN;
  return context_->status();
}

CefURLRequest::ErrorCode CefBrowserURLRequest::GetRequestError() {
  if (!VerifyContext())
    return ERR_NONE;
  return context_->response()->GetError();
}

CefRefPtr<CefResponse> CefBrowserURLRequest::GetResponse() {
  if (!VerifyContext())
    return NULL;
  return context_->response();
}

bool CefBrowserURLRequest::ResponseWasCached() {
  if (!VerifyContext())
    return false;
  return context_->response_was_cached();
}

void CefBrowserURLRequest::Cancel() {
  if (!VerifyContext())
    return;
  return context_->Cancel();
}

bool CefBrowserURLRequest::VerifyContext() {
  if (!context_->CalledOnValidThread()) {
    NOTREACHED() << "called on invalid thread";
    return false;
  }

  return true;
}
