// 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_file.h"

#include "base/base64.h"
#include "base/bind.h"
#include "base/files/file_util.h"
#include "base/sequenced_task_runner.h"
#include "base/strings/string_util.h"
#include "base/task/lazy_thread_pool_task_runner.h"
#include "base/third_party/icu/icu_utf.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "storage/browser/file_system/file_system_context.h"

namespace content {

scoped_refptr<base::SequencedTaskRunner> impl_task_runner() {
  constexpr base::TaskTraits kBlockingTraits = {
      base::MayBlock(), base::TaskPriority::BEST_EFFORT};
  static base::LazyThreadPoolSequencedTaskRunner s_sequenced_task_unner =
      LAZY_THREAD_POOL_SEQUENCED_TASK_RUNNER_INITIALIZER(kBlockingTraits);
  return s_sequenced_task_unner.Get();
}

scoped_refptr<DevToolsStreamFile> DevToolsStreamFile::Create(
    DevToolsIOContext* context,
    bool binary) {
  return new DevToolsStreamFile(context, binary);
}

DevToolsStreamFile::DevToolsStreamFile(DevToolsIOContext* context, bool binary)
    : DevToolsIOContext::Stream(impl_task_runner()),
      handle_(Register(context)),
      binary_(binary),
      task_runner_(impl_task_runner()),
      had_errors_(false),
      last_read_pos_(0) {}

DevToolsStreamFile::~DevToolsStreamFile() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
}

bool DevToolsStreamFile::InitOnFileSequenceIfNeeded() {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  if (had_errors_)
    return false;
  if (file_.IsValid())
    return true;
  base::FilePath temp_path;
  if (!base::CreateTemporaryFile(&temp_path)) {
    LOG(ERROR) << "Failed to create temporary file";
    had_errors_ = true;
    return false;
  }
  const unsigned flags = base::File::FLAG_OPEN_TRUNCATED |
                         base::File::FLAG_WRITE | base::File::FLAG_READ |
                         base::File::FLAG_DELETE_ON_CLOSE;
  file_.Initialize(temp_path, flags);
  if (!file_.IsValid()) {
    LOG(ERROR) << "Failed to open temporary file: " << temp_path.value() << ", "
               << base::File::ErrorToString(file_.error_details());
    had_errors_ = true;
    base::DeleteFile(temp_path);
    return false;
  }
  return true;
}

void DevToolsStreamFile::Read(off_t position,
                              size_t max_size,
                              ReadCallback callback) {
  task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&DevToolsStreamFile::ReadOnFileSequence, this,
                                position, max_size, std::move(callback)));
}

void DevToolsStreamFile::Append(std::unique_ptr<std::string> data) {
  task_runner_->PostTask(
      FROM_HERE, base::BindOnce(&DevToolsStreamFile::AppendOnFileSequence, this,
                                std::move(data)));
}

void DevToolsStreamFile::ReadOnFileSequence(off_t position,
                                            size_t max_size,
                                            ReadCallback callback) {
  DCHECK(task_runner_->RunsTasksInCurrentSequence());
  Status status = StatusFailure;
  std::unique_ptr<std::string> data;
  bool base64_encoded = false;

  if (file_.IsValid()) {
    std::string buffer;
    buffer.resize(max_size);
    if (position < 0)
      position = last_read_pos_;
    int size_got = file_.ReadNoBestEffort(position, &*buffer.begin(), max_size);
    if (size_got < 0) {
      LOG(ERROR) << "Failed to read temporary file";
      had_errors_ = true;
      file_.Close();
    } else {
      // Provided client has requested sufficient large block, make their
      // life easier by not truncating in the middle of a UTF-8 character.
      if (size_got > 6 && !CBU8_IS_SINGLE(buffer[size_got - 1])) {
        base::TruncateUTF8ToByteSize(buffer, size_got, &buffer);
        size_got = buffer.size();
      } else {
        buffer.resize(size_got);
      }
      data.reset(new std::string(std::move(buffer)));
      status = size_got ? StatusSuccess : StatusEOF;
      last_read_pos_ = position + size_got;
    }
  }
  if (binary_) {
    std::string raw_data(std::move(*data));
    base::Base64Encode(raw_data, data.get());
    base64_encoded = true;
  }
  GetUIThreadTaskRunner({})->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), std::move(data),
                                base64_encoded, status));
}

void DevToolsStreamFile::AppendOnFileSequence(
    std::unique_ptr<std::string> data) {
  if (!InitOnFileSequenceIfNeeded())
    return;
  int size_written = file_.WriteAtCurrentPos(&*data->begin(), data->length());
  if (size_written != static_cast<int>(data->length())) {
    LOG(ERROR) << "Failed to write temporary file";
    had_errors_ = true;
    file_.Close();
  }
}

}  // namespace content
