// 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 "content/browser/devtools/devtools_stream_blob.h"

#include "base/base64.h"
#include "base/bind.h"
#include "base/strings/string_piece.h"
#include "base/task/post_task.h"
#include "content/browser/blob_storage/chrome_blob_storage_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "net/base/io_buffer.h"
#include "storage/browser/blob/blob_data_handle.h"
#include "storage/browser/blob/blob_reader.h"
#include "storage/browser/blob/blob_storage_context.h"
#include "storage/common/blob_storage/blob_storage_constants.h"

namespace content {

using storage::BlobReader;

DevToolsStreamBlob::ReadRequest::ReadRequest(off_t position,
                                             size_t max_size,
                                             ReadCallback callback)
    : position(position), max_size(max_size), callback(std::move(callback)) {}

DevToolsStreamBlob::ReadRequest::~ReadRequest() = default;

DevToolsStreamBlob::DevToolsStreamBlob()
    : DevToolsIOContext::Stream(
          base::CreateSingleThreadTaskRunnerWithTraits({BrowserThread::IO})),
      last_read_pos_(0),
      failed_(false),
      is_binary_(false) {}

DevToolsStreamBlob::~DevToolsStreamBlob() {
  if (blob_reader_)
    blob_reader_->Kill();
}

namespace {
void UnregisterIfOpenFailed(base::WeakPtr<DevToolsIOContext> context,
                            const std::string& handle,
                            bool success) {
  if (!success && context)
    context->Close(handle);
}
}  // namespace

// static
scoped_refptr<DevToolsIOContext::Stream> DevToolsStreamBlob::Create(
    DevToolsIOContext* io_context,
    ChromeBlobStorageContext* blob_context,
    StoragePartition* partition,
    const std::string& handle,
    const std::string& uuid) {
  scoped_refptr<DevToolsStreamBlob> result = new DevToolsStreamBlob();
  result->Register(io_context, handle);
  result->Open(
      blob_context, partition, uuid,
      base::BindOnce(&UnregisterIfOpenFailed, io_context->AsWeakPtr(), handle));
  return std::move(result);
}

void DevToolsStreamBlob::ReadRequest::Fail() {
  base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI},
                           base::BindOnce(std::move(callback), nullptr, false,
                                          Stream::StatusFailure));
}

void DevToolsStreamBlob::Open(scoped_refptr<ChromeBlobStorageContext> context,
                              StoragePartition* partition,
                              const std::string& handle,
                              OpenCallback callback) {
  base::PostTaskWithTraits(
      FROM_HERE, {BrowserThread::IO},
      base::BindOnce(&DevToolsStreamBlob::OpenOnIO, this, context, handle,
                     std::move(callback)));
}

void DevToolsStreamBlob::Read(off_t position,
                              size_t max_size,
                              ReadCallback callback) {
  std::unique_ptr<ReadRequest> request(
      new ReadRequest(position, max_size, std::move(callback)));
  base::PostTaskWithTraits(
      FROM_HERE, {BrowserThread::IO},
      base::BindOnce(&DevToolsStreamBlob::ReadOnIO, this, std::move(request)));
}

void DevToolsStreamBlob::OpenOnIO(
    scoped_refptr<ChromeBlobStorageContext> blob_context,
    const std::string& uuid,
    OpenCallback callback) {
  DCHECK(!blob_handle_);

  storage::BlobStorageContext* bsc = blob_context->context();
  blob_handle_ = bsc->GetBlobDataFromUUID(uuid);
  if (!blob_handle_) {
    LOG(ERROR) << "No blob with uuid: " << uuid;
    FailOnIO(std::move(callback));
    return;
  }
  is_binary_ = !DevToolsIOContext::IsTextMimeType(blob_handle_->content_type());
  open_callback_ = std::move(callback);
  blob_handle_->RunOnConstructionComplete(
      base::BindOnce(&DevToolsStreamBlob::OnBlobConstructionComplete, this));
}

void DevToolsStreamBlob::OnBlobConstructionComplete(
    storage::BlobStatus status) {
  DCHECK(!BlobStatusIsPending(status));
  if (BlobStatusIsError(status)) {
    LOG(ERROR) << "Blob building failed: " << static_cast<int>(status);
    FailOnIO(std::move(open_callback_));
    return;
  }
  base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI},
                           base::BindOnce(std::move(open_callback_), true));
  if (!pending_reads_.empty())
    StartReadRequest();
}

void DevToolsStreamBlob::ReadOnIO(std::unique_ptr<ReadRequest> request) {
  if (failed_) {
    request->Fail();
    return;
  }
  pending_reads_.push(std::move(request));
  if (pending_reads_.size() > 1 || open_callback_)
    return;
  StartReadRequest();
}

void DevToolsStreamBlob::FailOnIO() {
  failed_ = true;
  while (!pending_reads_.empty()) {
    pending_reads_.front()->Fail();
    pending_reads_.pop();
  }
}

void DevToolsStreamBlob::FailOnIO(OpenCallback callback) {
  base::PostTaskWithTraits(FROM_HERE, {BrowserThread::UI},
                           base::BindOnce(std::move(callback), false));
  FailOnIO();
}

void DevToolsStreamBlob::StartReadRequest() {
  DCHECK_GE(pending_reads_.size(), 1UL);
  DCHECK(blob_handle_);
  DCHECK(!failed_);

  ReadRequest& request = *pending_reads_.front();
  if (request.position < 0)
    request.position = last_read_pos_;
  if (request.position != last_read_pos_)
    blob_reader_.reset();
  if (!blob_reader_)
    CreateReader();
  else
    BeginRead();
}

void DevToolsStreamBlob::BeginRead() {
  DCHECK_GE(pending_reads_.size(), 1UL);
  ReadRequest& request = *pending_reads_.front();
  if (!io_buf_ || static_cast<size_t>(io_buf_->size()) < request.max_size)
    io_buf_ = base::MakeRefCounted<net::IOBufferWithSize>(request.max_size);
  int bytes_read;
  BlobReader::Status status = blob_reader_->Read(
      io_buf_.get(), request.max_size, &bytes_read,
      base::BindOnce(&DevToolsStreamBlob::OnReadComplete, this));
  if (status == BlobReader::Status::IO_PENDING)
    return;
  // This is for uniformity with the asynchronous case.
  if (status == BlobReader::Status::NET_ERROR) {
    bytes_read = blob_reader_->net_error();
    DCHECK_LT(0, bytes_read);
  }
  base::PostTaskWithTraits(
      FROM_HERE, {BrowserThread::IO},
      base::BindOnce(&DevToolsStreamBlob::OnReadComplete, this, bytes_read));
}

void DevToolsStreamBlob::OnReadComplete(int bytes_read) {
  std::unique_ptr<ReadRequest> request = std::move(pending_reads_.front());
  pending_reads_.pop();

  Status status;
  std::unique_ptr<std::string> data(new std::string());
  bool base64_encoded = false;

  if (bytes_read < 0) {
    status = StatusFailure;
    LOG(ERROR) << "Error reading blob: " << net::ErrorToString(bytes_read);
  } else if (!bytes_read) {
    status = StatusEOF;
  } else {
    last_read_pos_ += bytes_read;
    status = blob_reader_->remaining_bytes() ? StatusSuccess : StatusEOF;
    if (is_binary_) {
      base64_encoded = true;
      Base64Encode(base::StringPiece(io_buf_->data(), bytes_read), data.get());
    } else {
      // TODO(caseq): truncate at UTF8 boundary.
      *data = std::string(io_buf_->data(), bytes_read);
    }
  }
  base::PostTaskWithTraits(
      FROM_HERE, {BrowserThread::IO},
      base::BindOnce(std::move(request->callback), std::move(data),
                     base64_encoded, status));
  if (!pending_reads_.empty())
    StartReadRequest();
}

void DevToolsStreamBlob::CreateReader() {
  DCHECK(!blob_reader_);
  blob_reader_ = blob_handle_->CreateReader();
  BlobReader::Status status = blob_reader_->CalculateSize(
      base::BindOnce(&DevToolsStreamBlob::OnCalculateSizeComplete, this));
  if (status != BlobReader::Status::IO_PENDING) {
    OnCalculateSizeComplete(status == BlobReader::Status::NET_ERROR
                                ? blob_reader_->net_error()
                                : net::OK);
  }
}

void DevToolsStreamBlob::OnCalculateSizeComplete(int net_error) {
  if (net_error != net::OK) {
    FailOnIO();
    return;
  }
  off_t seek_to = pending_reads_.front()->position;
  if (seek_to != 0UL) {
    if (seek_to >= static_cast<off_t>(blob_reader_->total_size())) {
      OnReadComplete(0);
      return;
    }
    BlobReader::Status status = blob_reader_->SetReadRange(
        seek_to, blob_reader_->total_size() - seek_to);
    if (status != BlobReader::Status::DONE) {
      FailOnIO();
      return;
    }
  }
  BeginRead();
}

}  // namespace content
