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

#include "third_party/blink/renderer/modules/clipboard/clipboard_writer.h"

#include "arkweb/build/features/features.h"
#include "base/task/single_thread_task_runner.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/clipboard/clipboard.mojom-blink.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_supported_type.h"
#include "third_party/blink/renderer/core/clipboard/system_clipboard.h"
#include "third_party/blink/renderer/core/dom/document_fragment.h"
#include "third_party/blink/renderer/core/editing/serializers/serialization.h"
#include "third_party/blink/renderer/core/execution_context/execution_context.h"
#include "third_party/blink/renderer/core/fileapi/file_reader_loader.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/web_feature.h"
#include "third_party/blink/renderer/core/imagebitmap/image_bitmap.h"
#include "third_party/blink/renderer/core/keywords.h"
#include "third_party/blink/renderer/core/typed_arrays/array_buffer/array_buffer_contents.h"
#include "third_party/blink/renderer/core/xml/dom_parser.h"
#include "third_party/blink/renderer/modules/clipboard/clipboard.h"
#include "third_party/blink/renderer/platform/heap/cross_thread_handle.h"
#include "third_party/blink/renderer/platform/image-decoders/image_decoder.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/scheduler/public/post_cross_thread_task.h"
#include "third_party/blink/renderer/platform/scheduler/public/worker_pool.h"
#include "third_party/blink/renderer/platform/wtf/cross_thread_copier_base.h"
#include "third_party/blink/renderer/platform/wtf/cross_thread_copier_skia.h"
#include "third_party/blink/renderer/platform/wtf/cross_thread_functional.h"
#include "third_party/blink/renderer/platform/wtf/wtf.h"
#include "ui/base/clipboard/clipboard_constants.h"

namespace blink {

namespace {  // anonymous namespace for ClipboardWriter's derived classes.

// Writes image/png content to the System Clipboard.
class ClipboardImageWriter final : public ClipboardWriter {
 public:
  ClipboardImageWriter(SystemClipboard* system_clipboard,
                       ClipboardPromise* promise)
      : ClipboardWriter(system_clipboard, promise) {}
  ~ClipboardImageWriter() override = default;

 private:
  void StartWrite(
      DOMArrayBuffer* raw_data,
      scoped_refptr<base::SingleThreadTaskRunner> task_runner) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    // ArrayBufferContents is a thread-safe smart pointer around the backing
    // store.
    ArrayBufferContents contents = *raw_data->Content();
    worker_pool::PostTask(
        FROM_HERE,
        CrossThreadBindOnce(&ClipboardImageWriter::DecodeOnBackgroundThread,
                            std::move(contents), MakeCrossThreadHandle(this),
                            task_runner));
  }
  static void DecodeOnBackgroundThread(
      ArrayBufferContents png_data,
      CrossThreadHandle<ClipboardImageWriter> writer,
      scoped_refptr<base::SingleThreadTaskRunner> task_runner) {
    DCHECK(!IsMainThread());
    std::unique_ptr<ImageDecoder> decoder = ImageDecoder::Create(
        SegmentReader::CreateFromSkData(
            SkData::MakeWithoutCopy(png_data.Data(), png_data.DataLength())),
        /*data_complete=*/true, ImageDecoder::kAlphaPremultiplied,
        ImageDecoder::kDefaultBitDepth, ColorBehavior::kTag,
#if !BUILDFLAG(ARKWEB_HEIF_SUPPORT)
        cc::AuxImage::kDefault,
#endif
        Platform::GetMaxDecodedImageBytes());
    sk_sp<SkImage> image = nullptr;
    // `decoder` is nullptr if `png_data` doesn't begin with the PNG signature.
    if (decoder) {
      image = ImageBitmap::GetSkImageFromDecoder(std::move(decoder));
    }

    PostCrossThreadTask(
        *task_runner, FROM_HERE,
        CrossThreadBindOnce(&ClipboardImageWriter::Write,
                            MakeUnwrappingCrossThreadHandle(std::move(writer)),
                            std::move(image)));
  }
  void Write(sk_sp<SkImage> image) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (!image) {
      promise_->RejectFromReadOrDecodeFailure();
      return;
    }
    if (!promise_->GetLocalFrame()) {
      return;
    }
    SkBitmap bitmap;
    image->asLegacyBitmap(&bitmap);
    system_clipboard()->WriteImage(std::move(bitmap));
    promise_->CompleteWriteRepresentation();
  }
};

// Writes text/plain content to the System Clipboard.
class ClipboardTextWriter final : public ClipboardWriter {
 public:
  ClipboardTextWriter(SystemClipboard* system_clipboard,
                      ClipboardPromise* promise)
      : ClipboardWriter(system_clipboard, promise) {}
  ~ClipboardTextWriter() override = default;

 private:
  void StartWrite(
      DOMArrayBuffer* raw_data,
      scoped_refptr<base::SingleThreadTaskRunner> task_runner) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    // ArrayBufferContents is a thread-safe smart pointer around the backing
    // store.
    ArrayBufferContents contents = *raw_data->Content();
    worker_pool::PostTask(
        FROM_HERE,
        CrossThreadBindOnce(&ClipboardTextWriter::DecodeOnBackgroundThread,
                            std::move(contents), MakeCrossThreadHandle(this),
                            task_runner));
  }
  static void DecodeOnBackgroundThread(
      ArrayBufferContents raw_data,
      CrossThreadHandle<ClipboardTextWriter> writer,
      scoped_refptr<base::SingleThreadTaskRunner> task_runner) {
    DCHECK(!IsMainThread());

    String wtf_string = String::FromUTF8(raw_data.ByteSpan());
    PostCrossThreadTask(
        *task_runner, FROM_HERE,
        CrossThreadBindOnce(&ClipboardTextWriter::Write,
                            MakeUnwrappingCrossThreadHandle(std::move(writer)),
                            std::move(wtf_string)));
  }
  void Write(const String& text) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (!promise_->GetLocalFrame()) {
      return;
    }
    system_clipboard()->WritePlainText(text);

    promise_->CompleteWriteRepresentation();
  }
};

// Writes text/html content to the System Clipboard.
class ClipboardHtmlWriter final : public ClipboardWriter {
 public:
  ClipboardHtmlWriter(SystemClipboard* system_clipboard,
                      ClipboardPromise* promise)
      : ClipboardWriter(system_clipboard, promise) {}
  ~ClipboardHtmlWriter() override = default;

 private:
  void StartWrite(
      DOMArrayBuffer* html_data,
      scoped_refptr<base::SingleThreadTaskRunner> task_runner) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    LocalFrame* local_frame = promise_->GetLocalFrame();
    auto* execution_context = promise_->GetExecutionContext();
    if (!local_frame || !execution_context) {
      return;
    }
    const KURL& url = local_frame->GetDocument()->Url();
    DOMParser* dom_parser = DOMParser::Create(promise_->GetScriptState());
    String html_string = String::FromUTF8(html_data->ByteSpan());
    const Document* doc = dom_parser->ParseFromStringWithoutTrustedTypes(
        html_string, V8SupportedType(V8SupportedType::Enum::kTextHtml));
    DCHECK(doc);
    String serialized_html = CreateMarkup(doc, kIncludeNode, kResolveAllURLs);
    Write(serialized_html, url);
  }

  void Write(const String& serialized_html, const KURL& url) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    system_clipboard()->WriteHTML(serialized_html, url);
    promise_->CompleteWriteRepresentation();
  }
};

// Write image/svg+xml content to the System Clipboard.
class ClipboardSvgWriter final : public ClipboardWriter {
 public:
  ClipboardSvgWriter(SystemClipboard* system_clipboard,
                     ClipboardPromise* promise)
      : ClipboardWriter(system_clipboard, promise) {}
  ~ClipboardSvgWriter() override = default;

 private:
  void StartWrite(
      DOMArrayBuffer* svg_data,
      scoped_refptr<base::SingleThreadTaskRunner> task_runner) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    LocalFrame* local_frame = promise_->GetLocalFrame();
    if (!local_frame) {
      return;
    }

    DOMParser* dom_parser = DOMParser::Create(promise_->GetScriptState());
    String svg_string = String::FromUTF8(svg_data->ByteSpan());
    const Document* doc = dom_parser->ParseFromStringWithoutTrustedTypes(
        svg_string, V8SupportedType(V8SupportedType::Enum::kImageSvgXml));
    promise_->GetExecutionContext()->CountUse(WebFeature::kClipboardSvgWrite);
    Write(CreateMarkup(doc, kIncludeNode, kResolveAllURLs));
  }

  void Write(const String& svg_html) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    system_clipboard()->WriteSvg(svg_html);
    promise_->CompleteWriteRepresentation();
  }
};

// Writes arbitrary, unsanitized content to the System Clipboard.
class ClipboardCustomFormatWriter final : public ClipboardWriter {
 public:
  ClipboardCustomFormatWriter(SystemClipboard* system_clipboard,
                              ClipboardPromise* promise,
                              const String& mime_type)
      : ClipboardWriter(system_clipboard, promise), mime_type_(mime_type) {}
  ~ClipboardCustomFormatWriter() override = default;

 private:
  void StartWrite(
      DOMArrayBuffer* custom_format_data,
      scoped_refptr<base::SingleThreadTaskRunner> task_runner) override {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);

    promise_->GetExecutionContext()->CountUse(
        WebFeature::kClipboardCustomFormatWrite);
    Write(custom_format_data);
  }

  void Write(DOMArrayBuffer* custom_format_data) {
    DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
    if (!promise_->GetLocalFrame()) {
      return;
    }
    if (custom_format_data->ByteLength() >=
        mojom::blink::ClipboardHost::kMaxDataSize) {
      promise_->RejectFromReadOrDecodeFailure();
      return;
    }
    mojo_base::BigBuffer buffer(custom_format_data->ByteSpan());
    system_clipboard()->WriteUnsanitizedCustomFormat(mime_type_,
                                                     std::move(buffer));
    promise_->CompleteWriteRepresentation();
  }

  String mime_type_;
};

}  // anonymous namespace

// ClipboardWriter functions.

// static
ClipboardWriter* ClipboardWriter::Create(SystemClipboard* system_clipboard,
                                         const String& mime_type,
                                         ClipboardPromise* promise) {
  CHECK(ClipboardItem::supports(mime_type));
  String web_custom_format = Clipboard::ParseWebCustomFormat(mime_type);
  if (!web_custom_format.empty()) {
    // We write the custom MIME type without the "web " prefix into the web
    // custom format map so native applications don't have to add any string
    // parsing logic to read format from clipboard.
    return MakeGarbageCollected<ClipboardCustomFormatWriter>(
        system_clipboard, promise, web_custom_format);
  }

  if (mime_type == ui::kMimeTypePng) {
    return MakeGarbageCollected<ClipboardImageWriter>(system_clipboard,
                                                      promise);
  }

  if (mime_type == ui::kMimeTypePlainText) {
    return MakeGarbageCollected<ClipboardTextWriter>(system_clipboard, promise);
  }

  if (mime_type == ui::kMimeTypeHtml) {
    return MakeGarbageCollected<ClipboardHtmlWriter>(system_clipboard, promise);
  }

  if (mime_type == ui::kMimeTypeSvg) {
    return MakeGarbageCollected<ClipboardSvgWriter>(system_clipboard, promise);
  }

  NOTREACHED()
      << "IsValidType() and Create() have inconsistent implementations.";
}

ClipboardWriter::ClipboardWriter(SystemClipboard* system_clipboard,
                                 ClipboardPromise* promise)
    : promise_(promise),
      clipboard_task_runner_(promise->GetExecutionContext()->GetTaskRunner(
          TaskType::kUserInteraction)),
      file_reading_task_runner_(promise->GetExecutionContext()->GetTaskRunner(
          TaskType::kFileReading)),
      system_clipboard_(system_clipboard) {}

ClipboardWriter::~ClipboardWriter() = default;

void ClipboardWriter::WriteToSystem(V8UnionBlobOrString* clipboard_item_data) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (clipboard_item_data->IsBlob()) {
    DCHECK(!file_reader_);
    file_reader_ = MakeGarbageCollected<FileReaderLoader>(
        this, std::move(file_reading_task_runner_));
    file_reader_->Start(clipboard_item_data->GetAsBlob()->GetBlobDataHandle());
  } else if (clipboard_item_data->IsString()) {
    DCHECK(RuntimeEnabledFeatures::ClipboardItemWithDOMStringSupportEnabled());
    std::string utf8_string = clipboard_item_data->GetAsString().Utf8();
    StartWrite(DOMArrayBuffer::Create(base::as_byte_span(utf8_string)),
               clipboard_task_runner_);
  } else {
    NOTREACHED();
  }
}

// FileReaderClient implementation.
void ClipboardWriter::DidFinishLoading(FileReaderData contents) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  DOMArrayBuffer* array_buffer = std::move(contents).AsDOMArrayBuffer();
  DCHECK(array_buffer);

  self_keep_alive_.Clear();
  file_reader_ = nullptr;

  StartWrite(array_buffer, clipboard_task_runner_);
}

void ClipboardWriter::DidFail(FileErrorCode error_code) {
  FileReaderAccumulator::DidFail(error_code);
  self_keep_alive_.Clear();
  file_reader_ = nullptr;
  promise_->RejectFromReadOrDecodeFailure();
}

void ClipboardWriter::Trace(Visitor* visitor) const {
  FileReaderAccumulator::Trace(visitor);
  visitor->Trace(promise_);
  visitor->Trace(system_clipboard_);
  visitor->Trace(file_reader_);
}

}  // namespace blink