// Copyright 2015 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "storage/browser/blob/blob_data_item.h"

#include <algorithm>
#include <memory>
#include <utility>
#include <vector>

#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "components/file_access/scoped_file_access_delegate.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "net/base/net_errors.h"
#include "services/network/public/cpp/data_pipe_to_source_stream.h"
#include "storage/browser/file_system/file_system_context.h"
#include "third_party/blink/public/common/blob/blob_utils.h"

namespace storage {

namespace {
const base::FilePath::CharType kFutureFileName[] =
    FILE_PATH_LITERAL("_future_name_");
}

class MojoDataItem : public BlobDataItem::DataHandle {
 public:
  explicit MojoDataItem(mojom::BlobDataItemPtr element)
      : item_(std::move(element)) {
    reader_.Bind(std::move(item_->reader));
  }

  // BlobDataItem::DataHandle implementation.
  uint64_t GetSize() const override { return item_->size; }

  void Read(mojo::ScopedDataPipeProducerHandle producer,
            uint64_t src_offset,
            uint64_t bytes_to_read,
            base::OnceCallback<void(int)> callback) override {
    reader_->Read(src_offset, bytes_to_read, std::move(producer),
                  std::move(callback));
  }

  uint64_t GetSideDataSize() const override { return item_->side_data_size; }

  void ReadSideData(
      base::OnceCallback<void(int, mojo_base::BigBuffer)> callback) override {
    reader_->ReadSideData(std::move(callback));
  }

  void PrintTo(::std::ostream* os) const override {
    // TODO(enne): this is tricky to implement, as it's synchronous.
    // PrintTo should ideally be asynchronous.  See: http://crbug.com/809821
    *os << "<MojoDataItem>";
  }

 protected:
  ~MojoDataItem() override = default;

  mojom::BlobDataItemPtr item_;
  mojo::Remote<mojom::BlobDataItemReader> reader_;
};

BlobDataItem::DataHandle::~DataHandle() = default;

// static
scoped_refptr<BlobDataItem> BlobDataItem::CreateBytes(
    base::span<const uint8_t> bytes) {
  auto item =
      base::WrapRefCounted(new BlobDataItem(Type::kBytes, 0, bytes.size()));
  item->bytes_.assign(bytes.begin(), bytes.end());
  return item;
}

// static
scoped_refptr<BlobDataItem> BlobDataItem::CreateBytesDescription(
    size_t length) {
  return base::WrapRefCounted(
      new BlobDataItem(Type::kBytesDescription, 0, length));
}

// static
scoped_refptr<BlobDataItem> BlobDataItem::CreateFile(
    base::FilePath path,
    uint64_t offset,
    uint64_t length,
    base::Time expected_modification_time,
    scoped_refptr<ShareableFileReference> file_ref,
    file_access::ScopedFileAccessDelegate::RequestFilesAccessIOCallback
        file_access) {
  auto item =
      base::WrapRefCounted(new BlobDataItem(Type::kFile, offset, length));
  item->path_ = std::move(path);
  item->expected_modification_time_ = std::move(expected_modification_time);
  item->file_ref_ = std::move(file_ref);
  item->file_access_ = std::move(file_access);
  // TODO(mek): DCHECK(!item->IsFutureFileItem()) when BlobDataBuilder has some
  // other way of slicing a future file.
  return item;
}

// static
scoped_refptr<BlobDataItem> BlobDataItem::CreateFutureFile(uint64_t offset,
                                                           uint64_t length,
                                                           uint64_t file_id) {
  auto item =
      base::WrapRefCounted(new BlobDataItem(Type::kFile, offset, length));
  std::string file_id_str = base::NumberToString(file_id);
  item->path_ = base::FilePath(kFutureFileName)
                    .AddExtension(base::FilePath::StringType(
                        file_id_str.begin(), file_id_str.end()));
  return item;
}

// static
scoped_refptr<BlobDataItem> BlobDataItem::CreateFileFilesystem(
    const FileSystemURL& url,
    uint64_t offset,
    uint64_t length,
    base::Time expected_modification_time,
    scoped_refptr<FileSystemContext> file_system_context,
    file_access::ScopedFileAccessDelegate::RequestFilesAccessIOCallback
        file_access) {
  auto item = base::WrapRefCounted(
      new BlobDataItem(Type::kFileFilesystem, offset, length));
  item->filesystem_url_ = url;
  item->expected_modification_time_ = std::move(expected_modification_time);
  item->file_system_context_ = std::move(file_system_context);
  item->file_access_ = std::move(file_access);
  return item;
}

// static
scoped_refptr<BlobDataItem> BlobDataItem::CreateReadableDataHandle(
    scoped_refptr<DataHandle> data_handle,
    uint64_t offset,
    uint64_t length) {
  DCHECK(data_handle);
  DCHECK_LE(offset, data_handle->GetSize());
  DCHECK_LE(length, (data_handle->GetSize() - offset));
  auto item = base::WrapRefCounted(
      new BlobDataItem(Type::kReadableDataHandle, offset, length));
  item->data_handle_ = std::move(data_handle);
  return item;
}

// static
scoped_refptr<BlobDataItem> BlobDataItem::CreateMojoDataItem(
    mojom::BlobDataItemPtr item) {
  auto handle = base::MakeRefCounted<MojoDataItem>(std::move(item));
  auto data_item = base::WrapRefCounted(
      new BlobDataItem(Type::kReadableDataHandle, 0, handle->GetSize()));
  DCHECK_GT(handle->GetSize(), 0u);
  data_item->data_handle_ = std::move(handle);
  return data_item;
}

bool BlobDataItem::IsFutureFileItem() const {
  if (type_ != Type::kFile)
    return false;
  const base::FilePath::StringType prefix(kFutureFileName);
  // The prefix shouldn't occur unless the user used "AppendFutureFile". We
  // DCHECK on AppendFile to make sure no one appends a future file.
  return base::StartsWith(path().value(), prefix, base::CompareCase::SENSITIVE);
}

uint64_t BlobDataItem::GetFutureFileID() const {
  DCHECK(IsFutureFileItem());
  uint64_t id = 0;
  bool success = base::StringToUint64(path().Extension().substr(1), &id);
  DCHECK(success) << path().Extension();
  return id;
}

BlobDataItem::BlobDataItem(Type type, uint64_t offset, uint64_t length)
    : type_(type), offset_(offset), length_(length) {}

BlobDataItem::~BlobDataItem() = default;

void BlobDataItem::AllocateBytes() {
  DCHECK_EQ(type_, Type::kBytesDescription);
  bytes_.resize(length_);
  type_ = Type::kBytes;
}

void BlobDataItem::PopulateBytes(base::span<const uint8_t> data) {
  DCHECK_EQ(type_, Type::kBytesDescription);
  DCHECK_EQ(length_, data.size());
  type_ = Type::kBytes;
  bytes_.assign(data.begin(), data.end());
}

void BlobDataItem::ShrinkBytes(size_t new_length) {
  DCHECK_EQ(type_, Type::kBytes);
  length_ = new_length;
  bytes_.resize(length_);
  bytes_.shrink_to_fit();
}

void BlobDataItem::PopulateFile(
    base::FilePath path,
    base::Time expected_modification_time,
    scoped_refptr<ShareableFileReference> file_ref) {
  DCHECK_EQ(type_, Type::kFile);
  DCHECK(IsFutureFileItem());
  path_ = std::move(path);
  expected_modification_time_ = std::move(expected_modification_time);
  file_ref_ = std::move(file_ref);
}

void BlobDataItem::ShrinkFile(uint64_t new_length) {
  DCHECK_EQ(type_, Type::kFile);
  DCHECK_LE(new_length, length_);
  length_ = new_length;
}

void BlobDataItem::GrowFile(uint64_t new_length) {
  DCHECK_EQ(type_, Type::kFile);
  DCHECK_GE(new_length, length_);
  length_ = new_length;
}

// static
void BlobDataItem::SetFileModificationTimes(
    std::vector<scoped_refptr<BlobDataItem>> items,
    std::vector<base::Time> times) {
  DCHECK_EQ(items.size(), times.size());
  for (size_t i = 0; i < items.size(); ++i) {
    items[i]->expected_modification_time_ = times[i];
  }
}

void PrintTo(const BlobDataItem& x, ::std::ostream* os) {
  const uint64_t kMaxDataPrintLength = 40;
  DCHECK(os);
  *os << "<BlobDataItem>{type: ";
  switch (x.type()) {
    case BlobDataItem::Type::kBytes: {
      uint64_t length = std::min(x.length(), kMaxDataPrintLength);
      *os << "kBytes, data: ["
          << base::HexEncode(x.bytes().first(static_cast<size_t>(length)));
      if (length < x.length()) {
        *os << "<...truncated due to length...>";
      }
      *os << "]";
      break;
    }
    case BlobDataItem::Type::kBytesDescription:
      *os << "kBytesDescription";
      break;
    case BlobDataItem::Type::kFile:
      *os << "kFile, path: " << x.path().AsUTF8Unsafe()
          << ", expected_modification_time: " << x.expected_modification_time();
      break;
    case BlobDataItem::Type::kFileFilesystem:
      *os << "kFileFilesystem, url: " << x.filesystem_url().DebugString();
      break;
    case BlobDataItem::Type::kReadableDataHandle:
      *os << "kReadableDataHandle"
          << ", data_handle_: ";
      x.data_handle()->PrintTo(os);
      break;
  }
  *os << ", length: " << x.length() << ", offset: " << x.offset()
      << ", has_data_handle: " << (x.data_handle_.get() ? "true" : "false");
}

bool operator==(const BlobDataItem& a, const BlobDataItem& b) {
  if (a.type() != b.type() || a.offset() != b.offset() ||
      a.length() != b.length())
    return false;
  switch (a.type()) {
    case BlobDataItem::Type::kBytes:
      return std::ranges::equal(a.bytes(), b.bytes());
    case BlobDataItem::Type::kBytesDescription:
      return true;
    case BlobDataItem::Type::kFile:
      return a.path() == b.path() &&
             a.expected_modification_time() == b.expected_modification_time();
    case BlobDataItem::Type::kFileFilesystem:
      return a.filesystem_url() == b.filesystem_url();
    case BlobDataItem::Type::kReadableDataHandle:
      return a.data_handle() == b.data_handle();
  }
  NOTREACHED();
}

bool operator!=(const BlobDataItem& a, const BlobDataItem& b) {
  return !(a == b);
}

}  // namespace storage
