// Copyright 2012 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/x11_util.h"

#include "base/containers/span.h"
#include "base/functional/bind.h"
#include "base/strings/string_util.h"
#include "base/types/cxx23_to_underlying.h"
#include "remoting/base/logging.h"
#include "ui/gfx/x/future.h"
#include "ui/gfx/x/randr.h"
#include "ui/gfx/x/xinput.h"
#include "ui/gfx/x/xproto_types.h"
#include "ui/gfx/x/xtest.h"

namespace remoting {

bool IgnoreXServerGrabs(x11::Connection* connection, bool ignore) {
  if (!connection->xtest().present()) {
    return false;
  }

  connection->xtest().GrabControl({ignore});
  return true;
}

bool IsVirtualSession(x11::Connection* connection) {
  // Try to identify a virtual session. Since there's no way to tell from the
  // vendor string, we check for known virtual input devices.
  // For Xorg+video-dummy, there is no input device that is specific to CRD, so
  // we just check if all input devices are virtual and if the display outputs
  // are all DUMMY*.
  // TODO(lambroslambrou): Find a similar way to determine that the *output* is
  // secure.
  if (!connection->xinput().present()) {
    // If XInput is not available, assume it is not a virtual session.
    LOG(ERROR) << "X Input extension not available";
    return false;
  }

  auto devices = connection->xinput().ListInputDevices().Sync();
  if (!devices) {
    LOG(ERROR) << "ListInputDevices failed";
    return false;
  }

  bool found_xvfb_mouse = false;
  bool found_xvfb_keyboard = false;
  bool found_other_devices = false;
  for (size_t i = 0; i < devices->devices.size(); i++) {
    const auto& device_info = devices->devices[i];
    const std::string& name = devices->names[i].name;
    if (device_info.device_use == x11::Input::DeviceUse::IsXExtensionPointer) {
      if (name == "Xvfb mouse") {
        found_xvfb_mouse = true;
      } else if (name == "Chrome Remote Desktop Input") {
        // Ignored. This device only exists if xserver-xorg-input-void is
        // installed, which is no longer available since Debian 11, so we can't
        // use it to reliably check if the user is on Xorg+video-dummy.
      } else if (name != "Virtual core XTEST pointer") {
        found_other_devices = true;
        HOST_LOG << "Non-virtual mouse found: " << name;
      }
    } else if (device_info.device_use ==
               x11::Input::DeviceUse::IsXExtensionKeyboard) {
      if (name == "Xvfb keyboard") {
        found_xvfb_keyboard = true;
      } else if (name != "Virtual core XTEST keyboard") {
        found_other_devices = true;
        HOST_LOG << "Non-virtual keyboard found: " << name;
      }
    } else if (device_info.device_use == x11::Input::DeviceUse::IsXPointer) {
      if (name != "Virtual core pointer") {
        found_other_devices = true;
        HOST_LOG << "Non-virtual mouse found: " << name;
      }
    } else if (device_info.device_use == x11::Input::DeviceUse::IsXKeyboard) {
      if (name != "Virtual core keyboard") {
        found_other_devices = true;
        HOST_LOG << "Non-virtual keyboard found: " << name;
      }
    } else {
      found_other_devices = true;
      HOST_LOG << "Non-virtual device found: " << name;
    }
  }
  return ((found_xvfb_mouse && found_xvfb_keyboard) ||
          IsUsingVideoDummyDriver(connection)) &&
         !found_other_devices;
}

bool IsUsingVideoDummyDriver(x11::Connection* connection) {
  if (!connection->randr().present()) {
    // If RANDR is not available, assume it is not using a video dummy driver.
    LOG(ERROR) << "RANDR extension not available";
    return false;
  }

  auto root = connection->default_root();
  auto randr = connection->randr();
  auto resources = randr.GetScreenResourcesCurrent({root}).Sync();
  if (!resources) {
    LOG(ERROR) << "Cannot get screen resources";
    return false;
  }
  if (resources->outputs.empty()) {
    LOG(ERROR) << "RANDR returns no outputs";
    return false;
  }
  bool has_only_dummy_outputs = true;
  for (x11::RandR::Output output : resources->outputs) {
    auto output_info =
        randr
            .GetOutputInfo({.output = output,
                            .config_timestamp = resources->config_timestamp})
            .Sync();
    if (!output_info) {
      LOG(WARNING) << "Cannot get info for output "
                   << base::to_underlying(output);
      continue;
    }
    auto* output_name = reinterpret_cast<char*>(output_info->name.data());
    if (!base::StartsWith(output_name, "DUMMY")) {
      HOST_LOG << "Non-dummy output found: " << output_name;
      has_only_dummy_outputs = false;
      break;
    }
  }
  return has_only_dummy_outputs;
}

x11::Atom GetX11Atom(x11::Connection* connection, const std::string& name) {
  auto reply = connection->InternAtom({false, name}).Sync();
  if (!reply) {
    LOG(ERROR) << "Failed to intern atom " << name;
    return x11::Atom::None;
  }
  return reply->atom;
}

void SetOutputPhysicalSizeInMM(x11::Connection* connection,
                               x11::RandR::Output output,
                               int width,
                               int height) {
  static const x11::Atom width_mm_atom = GetX11Atom(connection, "WIDTH_MM");
  static const x11::Atom height_mm_atom = GetX11Atom(connection, "HEIGHT_MM");
  if (width_mm_atom == x11::Atom::None || height_mm_atom == x11::Atom::None) {
    return;
  }

  auto width_32 = static_cast<uint32_t>(width);
  auto height_32 = static_cast<uint32_t>(height);

  x11::RandR::ChangeOutputPropertyRequest request = {
      .output = output,
      .property = width_mm_atom,
      .type = x11::Atom::INTEGER,
      .format = 32,
      .mode = x11::PropMode::Replace,
      .num_units = 1,
      .data = base::MakeRefCounted<base::RefCountedStaticMemory>(
          base::byte_span_from_ref(width_32)),
  };
  connection->randr().ChangeOutputProperty(request).Sync();

  request.property = height_mm_atom;
  request.data = base::MakeRefCounted<base::RefCountedStaticMemory>(
      base::byte_span_from_ref(height_32));
  connection->randr().ChangeOutputProperty(request).Sync();
}

}  // namespace remoting