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

#include "remoting/host/linux/clipboard_gnome.h"

#include <algorithm>
#include <array>
#include <string_view>
#include <utility>

#include "base/functional/bind.h"
#include "remoting/base/constants.h"
#include "remoting/base/logging.h"
#include "remoting/host/linux/dbus_interfaces/org_gnome_Mutter_RemoteDesktop.h"
#include "remoting/proto/event.pb.h"
#include "remoting/protocol/clipboard_stub.h"

namespace remoting {

namespace {

constexpr char kRemoteDesktopBusName[] = "org.gnome.Mutter.RemoteDesktop";

// This list was created by looking at the MIME types claimed by some Wayland
// and XWayland apps that put text onto the clipboard. It is ordered by
// priority, preferring modern explicit UTF-8 formats over legacy string
// formats.
constexpr std::array<std::string_view, 5> kTextMimeTypes = {
    "text/plain;charset=utf-8", "UTF8_STRING", "text/plain", "STRING", "TEXT"};

}  // namespace

ClipboardGnome::ClipboardGnome(GDBusConnectionRef connection,
                               gvariant::ObjectPath session_path)
    : dbus_connection_(std::move(connection)),
      session_path_(std::move(session_path)) {}

ClipboardGnome::~ClipboardGnome() = default;

void ClipboardGnome::Start(
    std::unique_ptr<protocol::ClipboardStub> client_clipboard) {
  client_clipboard_.swap(client_clipboard);

  selection_owner_changed_signal_ = dbus_connection_.SignalSubscribe<
      org_gnome_Mutter_RemoteDesktop_Session::SelectionOwnerChanged>(
      kRemoteDesktopBusName, session_path_,
      base::BindRepeating(&ClipboardGnome::OnSelectionOwnerChanged,
                          weak_factory_.GetWeakPtr()));
  selection_transfer_signal_ = dbus_connection_.SignalSubscribe<
      org_gnome_Mutter_RemoteDesktop_Session::SelectionTransfer>(
      kRemoteDesktopBusName, session_path_,
      base::BindRepeating(&ClipboardGnome::OnSelectionTransfer,
                          weak_factory_.GetWeakPtr()));

  dbus_connection_
      .Call<org_gnome_Mutter_RemoteDesktop_Session::EnableClipboard>(
          kRemoteDesktopBusName, session_path_,
          std::tuple(gvariant::EmptyArrayOf<"{sv}">()),
          base::BindOnce([](base::expected<std::tuple<>, Loggable> result) {
            if (!result.has_value()) {
              LOG(ERROR) << "EnableClipboard failed: " << result.error();
            }
          }));
}

void ClipboardGnome::InjectClipboardEvent(
    const protocol::ClipboardEvent& event) {
  if (event.mime_type() != kMimeTypeTextUtf8) {
    LOG(ERROR) << "Non UTF-8 clipboard type from client: " << event.mime_type();
    return;
  }

  clipboard_data_ = std::move(event.data());

  // Claim ownership of the text mime-types. After this, GNOME may request the
  // clipboard data via one or more SelectionTransfer signals.

  // The variant type here is inconsistent with what GNOME returns in the
  // SelectionOwnerChanged signal. Here it must be "as", not "(as)":
  // https://gitlab.gnome.org/GNOME/gnome-remote-desktop/-/blob/979631ffeac75f79fad104c3e479dd7513cc2478/src/grd-session.c#L804
  auto mime_types_option = GVariantFrom(gvariant::BoxedRef(kTextMimeTypes));
  dbus_connection_.Call<org_gnome_Mutter_RemoteDesktop_Session::SetSelection>(
      kRemoteDesktopBusName, session_path_,
      std::tuple(std::array{std::pair{"mime-types", mime_types_option}}),
      base::BindOnce([](base::expected<std::tuple<>, Loggable> result) {
        if (!result.has_value()) {
          LOG(ERROR) << "SetSelection failed: " << result.error();
        }
      }));
}

void ClipboardGnome::OnSelectionOwnerChanged(
    std::tuple<GVariantRef<"a{sv}">> args) {
  HOST_LOG << "Received SelectionOwnerChanged signal.";

  auto& [options] = args;

  auto maybe_boxed_is_owner = options.LookUp("session-is-owner");
  if (maybe_boxed_is_owner.has_value()) {
    // "session-is-owner" is a variant holding a boolean.
    if (*maybe_boxed_is_owner == GVariantFrom(gvariant::Boxed{true})) {
      // The selection is now owned by this D-Bus caller (a recent SetSelection
      // call succeeded). SelectionRead should only be used to request the data
      // from a different owner.
      HOST_LOG << "Ignoring event, session is already owner.";
      return;
    }
  }

  auto maybe_boxed_mime_types = options.LookUp("mime-types");
  if (maybe_boxed_mime_types.has_value()) {
    std::vector<std::string> mime_types;
    // std::tie is needed here because mime-types has type "(as)", not "as".
    auto destructure_result =
        maybe_boxed_mime_types->TryDestructure(std::tie(mime_types));
    if (destructure_result.has_value()) {
      for (auto mime_type : kTextMimeTypes) {
        if (std::ranges::contains(mime_types, mime_type)) {
          SelectionRead(mime_type);
          return;
        }
      }
    }
  }
}

void ClipboardGnome::OnSelectionTransfer(
    std::tuple<std::string, std::uint32_t> args) {
  const auto& [mime_type, serial] = args;
  HOST_LOG << "Got SelectionTransfer signal with mime-type: " << mime_type;

  if (!std::ranges::contains(kTextMimeTypes, mime_type)) {
    // SelectionTransfer request should be for a mime-type claimed by
    // SetSelection.
    LOG(ERROR) << "Unexpected mime-type requested: " << mime_type;
    return;
  }

  SelectionWrite(serial);
}

void ClipboardGnome::SelectionRead(std::string_view mime_type) {
  dbus_connection_.Call<org_gnome_Mutter_RemoteDesktop_Session::SelectionRead>(
      kRemoteDesktopBusName, session_path_, std::tuple(mime_type),
      base::BindOnce(&ClipboardGnome::OnSelectionReadReply,
                     weak_factory_.GetWeakPtr()));
}

void ClipboardGnome::OnSelectionReadReply(
    base::expected<std::pair<std::tuple<GDBusFdList::Handle>, GDBusFdList>,
                   Loggable> result) {
  if (!result.has_value()) {
    LOG(ERROR) << "SelectionRead failed: " << result.error();
    return;
  }

  auto& [tuple, dbus_fds] = result.value();
  auto& [read_handle] = tuple;
  SparseFdList fd_list = std::move(dbus_fds).MakeSparse();
  base::ScopedFD read_fd = fd_list.Extract(read_handle);
  if (!read_fd.is_valid()) {
    LOG(ERROR) << "Failed to get FD for SelectionRead.";
    return;
  }

  fd_string_reader_ = FdStringReader::ReadFromPipe(
      std::move(read_fd), base::BindOnce(&ClipboardGnome::OnSelectionDataRead,
                                         weak_factory_.GetWeakPtr()));
}

void ClipboardGnome::OnSelectionDataRead(
    base::expected<std::string, Loggable> result) {
  // Close the FD.
  fd_string_reader_.reset();
  if (result.has_value()) {
    HOST_LOG << "Got clipboard data of size: " << result.value().size();
    protocol::ClipboardEvent event;
    event.set_mime_type(kMimeTypeTextUtf8);
    event.set_data(std::move(result.value()));
    client_clipboard_->InjectClipboardEvent(event);
  } else {
    LOG(ERROR) << "Failed to read clipboard data: " << result.error();
  }
}

void ClipboardGnome::SelectionWrite(std::uint32_t serial) {
  selection_write_serial_ = serial;
  dbus_connection_.Call<org_gnome_Mutter_RemoteDesktop_Session::SelectionWrite>(
      kRemoteDesktopBusName, session_path_, std::tuple(serial),
      base::BindOnce(&ClipboardGnome::OnSelectionWriteReply,
                     weak_factory_.GetWeakPtr()));
}

void ClipboardGnome::OnSelectionWriteReply(
    base::expected<std::pair<std::tuple<GDBusFdList::Handle>, GDBusFdList>,
                   Loggable> result) {
  if (!result.has_value()) {
    LOG(ERROR) << "SelectionWrite failed: " << result.error();
    return;
  }

  auto& [tuple, dbus_fds] = result.value();
  auto& [write_handle] = tuple;
  SparseFdList fd_list = std::move(dbus_fds).MakeSparse();
  base::ScopedFD write_fd = fd_list.Extract(write_handle);
  if (!write_fd.is_valid()) {
    LOG(ERROR) << "Failed to get FD for SelectionWrite.";
    return;
  }

  fd_string_writer_ = FdStringWriter::Write(
      clipboard_data_, std::move(write_fd),
      base::BindOnce(&ClipboardGnome::OnSelectionDataWritten,
                     weak_factory_.GetWeakPtr()));
}

void ClipboardGnome::OnSelectionDataWritten(
    base::expected<void, Loggable> result) {
  // Close the FD.
  fd_string_writer_.reset();
  bool success = result.has_value();
  if (!success) {
    LOG(ERROR) << "Failed to write clipboard data: " << result.error();
  }

  dbus_connection_
      .Call<org_gnome_Mutter_RemoteDesktop_Session::SelectionWriteDone>(
          kRemoteDesktopBusName, session_path_,
          std::tuple(selection_write_serial_, success),
          base::BindOnce([](base::expected<std::tuple<>, Loggable> result) {
            if (!result.has_value()) {
              LOG(ERROR) << "SelectionWriteDone returned error: "
                         << result.error();
            }
          }));
}

}  // namespace remoting
