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

#include "ui/gfx/x/x11_crtc_resizer.h"

#include <algorithm>
#include <iterator>
#include <utility>
#include <vector>

#include "base/check.h"
#include "base/containers/adapters.h"
#include "base/logging.h"
#include "base/stl_util.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/x/connection.h"
#include "ui/gfx/x/future.h"

namespace {

constexpr auto kInvalidMode = static_cast<x11::RandR::Mode>(0);
constexpr auto kDisabledCrtc = static_cast<x11::RandR::Crtc>(0);

// TODO(jamiewalch): Use the correct DPI for the mode: http://crbug.com/172405.
const int kDefaultDPI = 96;

int PixelsToMillimeters(int pixels, int dpi) {
  DCHECK(dpi != 0);

  const double kMillimetersPerInch = 25.4;

  // (pixels / dpi) is the length in inches. Multiplying by
  // kMillimetersPerInch converts to mm. Multiplication is done first to
  // avoid integer division.
  return static_cast<int>(kMillimetersPerInch * pixels / dpi);
}

}  // namespace

namespace x11 {

X11CrtcResizer::CrtcInfo::CrtcInfo() = default;
X11CrtcResizer::CrtcInfo::CrtcInfo(
    x11::RandR::Crtc crtc,
    int16_t x,
    int16_t y,
    uint16_t width,
    uint16_t height,
    x11::RandR::Mode mode,
    x11::RandR::Rotation rotation,
    const std::vector<x11::RandR::Output>& outputs)
    : crtc(crtc),
      old_x(x),
      x(x),
      old_y(y),
      y(y),
      width(width),
      height(height),
      mode(mode),
      rotation(rotation),
      outputs(outputs) {}
X11CrtcResizer::CrtcInfo::CrtcInfo(const X11CrtcResizer::CrtcInfo&) = default;
X11CrtcResizer::CrtcInfo::CrtcInfo(X11CrtcResizer::CrtcInfo&&) = default;
X11CrtcResizer::CrtcInfo& X11CrtcResizer::CrtcInfo::operator=(
    const X11CrtcResizer::CrtcInfo&) = default;
X11CrtcResizer::CrtcInfo& X11CrtcResizer::CrtcInfo::operator=(
    X11CrtcResizer::CrtcInfo&&) = default;
X11CrtcResizer::CrtcInfo::~CrtcInfo() = default;

bool X11CrtcResizer::CrtcInfo::OffsetsChanged() const {
  return old_x != x || old_y != y;
}

X11CrtcResizer::X11CrtcResizer(
    x11::RandR::GetScreenResourcesCurrentReply* resources,
    x11::Connection* connection)
    : connection_(connection) {
  if (resources != nullptr) {
    resources_ = *resources;
  }
  // Unittests provide nullptr, and do not exercise code-paths that talk to the
  // X server.
  if (connection_) {
    randr_ = &connection_->randr();
    auto atom_reply = connection_->InternAtom({false, "WM_STATE"}).Sync();
    if (atom_reply) {
      wm_state_atom_ = atom_reply->atom;
    } else {
      LOG(ERROR) << "Failed to intern atom for WM_STATE.";
    }
  }
}

X11CrtcResizer::~X11CrtcResizer() = default;

void X11CrtcResizer::FetchActiveCrtcs() {
  active_crtcs_.clear();
  x11::Time config_timestamp = resources_.config_timestamp;
  for (const auto& crtc : resources_.crtcs) {
    auto response = randr_->GetCrtcInfo({crtc, config_timestamp}).Sync();
    if (!response) {
      continue;
    }
    if (response->outputs.empty()) {
      continue;
    }

    AddCrtcFromReply(crtc, *response.reply);
  }
}

x11::RandR::Crtc X11CrtcResizer::GetCrtcForOutput(
    x11::RandR::Output output) const {
  // This implementation assumes an output is attached to only one CRTC. If
  // there are multiple CRTCs for the output, only the first will be returned,
  // but this should never occur with Xorg+video-dummy.
  auto iter =
      std::ranges::find_if(active_crtcs_, [output](const CrtcInfo& crtc_info) {
        return std::ranges::contains(crtc_info.outputs, output);
      });
  if (iter == active_crtcs_.end()) {
    return kDisabledCrtc;
  }
  return iter->crtc;
}

void X11CrtcResizer::DisableCrtc(x11::RandR::Crtc crtc) {
  x11::Time config_timestamp = resources_.config_timestamp;
  randr_->SetCrtcConfig({
      .crtc = crtc,
      .timestamp = x11::Time::CurrentTime,
      .config_timestamp = config_timestamp,
      .x = 0,
      .y = 0,
      .mode = kInvalidMode,
      .rotation = x11::RandR::Rotation::Rotate_0,
      .outputs = {},
  });
}

void X11CrtcResizer::UpdateActiveCrtcs(x11::RandR::Crtc crtc,
                                       x11::RandR::Mode mode,
                                       const gfx::Size& new_size) {
  updated_crtcs_.insert(crtc);

  // Find |crtc| in |active_crtcs_| and adjust its mode and size.
  auto iter = std::ranges::find(active_crtcs_, crtc, &CrtcInfo::crtc);

  // |crtc| was returned by GetCrtcForOutput() so it should definitely be in
  // the list.
  DCHECK(iter != active_crtcs_.end());

  iter->mode = mode;
  RelayoutCrtcs(iter->crtc, new_size);
  NormalizeCrtcs();
}

void X11CrtcResizer::UpdateActiveCrtc(x11::RandR::Crtc crtc,
                                      x11::RandR::Mode mode,
                                      const gfx::Rect& new_rect) {
  updated_crtcs_.insert(crtc);

  // Find |crtc| in |active_crtcs_| and adjust its mode and size.
  auto iter = std::ranges::find(active_crtcs_, crtc, &CrtcInfo::crtc);

  // |crtc| was returned by GetCrtcForOutput() so it should definitely be in
  // the list.
  DCHECK(iter != active_crtcs_.end());

  iter->mode = mode;
  iter->x = new_rect.x();
  iter->y = new_rect.y();
  iter->width = new_rect.width();
  iter->height = new_rect.height();
}

void X11CrtcResizer::AddActiveCrtc(
    x11::RandR::Crtc crtc,
    x11::RandR::Mode mode,
    const std::vector<x11::RandR::Output>& outputs,
    const gfx::Rect& new_rect) {
  // |crtc| is not active so it must not be in |active_crtcs_|.
  DCHECK(std::ranges::find(active_crtcs_, crtc, &CrtcInfo::crtc) ==
         active_crtcs_.end());

  active_crtcs_.emplace_back(crtc, new_rect.x(), new_rect.y(), new_rect.width(),
                             new_rect.height(), mode,
                             x11::RandR::Rotation::Rotate_0, outputs);
  updated_crtcs_.insert(crtc);
}

void X11CrtcResizer::RemoveActiveCrtc(x11::RandR::Crtc crtc) {
  auto to_remove = std::ranges::remove(active_crtcs_, crtc, &CrtcInfo::crtc);
  DCHECK(!to_remove.empty());
  active_crtcs_.erase(to_remove.begin(), to_remove.end());
}

void X11CrtcResizer::RelayoutCrtcs(x11::RandR::Crtc crtc_to_resize,
                                   const gfx::Size& new_size) {
  if (LayoutIsVertical()) {
    PackVertically(new_size, crtc_to_resize);
  } else {
    PackHorizontally(new_size, crtc_to_resize);
  }
}

void X11CrtcResizer::DisableChangedCrtcs() {
  for (const auto& crtc_info : active_crtcs_) {
    // Updated CRTCs are expected to be disabled by the caller.
    if (crtc_info.OffsetsChanged() &&
        !updated_crtcs_.contains(crtc_info.crtc)) {
      DisableCrtc(crtc_info.crtc);
    }
  }
}

void X11CrtcResizer::NormalizeCrtcs() {
  gfx::Rect bounding_box;
  for (const auto& crtc : active_crtcs_) {
    bounding_box.Union(gfx::Rect(crtc.x, crtc.y, crtc.width, crtc.height));
  }
  bounding_box_size_ = bounding_box.size();
  gfx::Vector2d adjustment =
      gfx::Vector2d(-bounding_box.origin().x(), -bounding_box.origin().y());
  if (adjustment.IsZero()) {
    return;
  }
  for (auto& crtc : active_crtcs_) {
    crtc.x += adjustment.x();
    crtc.y += adjustment.y();
  }
}

void X11CrtcResizer::MoveApplicationWindows() {
  if (!connection_) {
    // connection_ is nullptr in unittests.
    return;
  }

  // Only direct descendants of the root window should be moved. Child
  // windows automatically track the location of their parents, and can only
  // be moved within their parent window.
  auto query_response =
      connection_->QueryTree({connection_->default_root()}).Sync();
  if (!query_response) {
    return;
  }
  for (const auto& window : query_response->children) {
    auto attributes_response =
        connection_->GetWindowAttributes({window}).Sync();
    if (!attributes_response) {
      continue;
    }
    if (attributes_response->map_state != x11::MapState::Viewable) {
      // Unmapped or hidden windows can be left alone - their geometries
      // might not be meaningful. If the window later becomes mapped, the
      // window-manager will be responsible for its placement.
      continue;
    }
    auto geometry_response = connection_->GetGeometry(window).Sync();
    if (!geometry_response) {
      continue;
    }

    // Look for any CRTC which contains the window's top-left corner. If the
    // CRTC is being moved, request the window to be moved the same amount.
    for (const auto& crtc_info : active_crtcs_) {
      if (!crtc_info.OffsetsChanged()) {
        continue;
      }

      auto old_rect = gfx::Rect(crtc_info.old_x, crtc_info.old_y,
                                crtc_info.width, crtc_info.height);
      gfx::Vector2d window_top_left(geometry_response->x, geometry_response->y);
      if (!old_rect.Contains(
              gfx::Point(window_top_left.x(), window_top_left.y()))) {
        continue;
      }

      gfx::Vector2d adjustment(crtc_info.x - crtc_info.old_x,
                               crtc_info.y - crtc_info.old_y);
      window_top_left = window_top_left + adjustment;

      MoveWindow(window, *attributes_response.reply,
                 gfx::Point(window_top_left.x(), window_top_left.y()));
      break;
    }
  }
}

gfx::Size X11CrtcResizer::GetBoundingBox() const {
  DCHECK(!bounding_box_size_.IsEmpty());
  return bounding_box_size_;
}

void X11CrtcResizer::ApplyActiveCrtcs() {
  for (const auto& crtc_info : active_crtcs_) {
    if (crtc_info.OffsetsChanged() || updated_crtcs_.contains(crtc_info.crtc)) {
      x11::Time config_timestamp = resources_.config_timestamp;
      randr_->SetCrtcConfig({
          .crtc = crtc_info.crtc,
          .timestamp = x11::Time::CurrentTime,
          .config_timestamp = config_timestamp,
          .x = crtc_info.x,
          .y = crtc_info.y,
          .mode = crtc_info.mode,
          .rotation = crtc_info.rotation,
          .outputs = crtc_info.outputs,
      });
    }
  }
  updated_crtcs_.clear();
}

void X11CrtcResizer::UpdateRootWindow(x11::Window root) {
  // Disable any CRTCs that have been changed, so that the root window can be
  // safely resized to the bounding-box of the new CRTCs.
  // This is non-optimal: the only CRTCs that need disabling are those whose
  // original rectangles don't fit into the new root window - they are the ones
  // that would prevent resizing the root window. But figuring these out would
  // involve keeping track of all the original rectangles as well as the new
  // ones. So, to keep the implementation simple (and working for any arbitrary
  // layout algorithm), all changed CRTCs are disabled here.
  DisableChangedCrtcs();

  // Get the dimensions to resize the root window to.
  auto dimensions = GetBoundingBox();

  // TODO(lambroslambrou): Use the DPI from client size information.
  uint32_t width_mm = PixelsToMillimeters(dimensions.width(), kDefaultDPI);
  uint32_t height_mm = PixelsToMillimeters(dimensions.height(), kDefaultDPI);
  randr_->SetScreenSize({root, static_cast<uint16_t>(dimensions.width()),
                         static_cast<uint16_t>(dimensions.height()), width_mm,
                         height_mm});

  MoveApplicationWindows();

  // Apply the new CRTCs, which will re-enable any that were disabled.
  ApplyActiveCrtcs();
}

void X11CrtcResizer::SetCrtcsForTest(
    std::vector<x11::RandR::GetCrtcInfoReply> crtcs) {
  int id = 1;
  for (const auto& crtc : crtcs) {
    AddCrtcFromReply(static_cast<x11::RandR::Crtc>(id), crtc);
    id++;
  }
}

std::vector<gfx::Rect> X11CrtcResizer::GetCrtcsForTest() const {
  std::vector<gfx::Rect> result;
  for (const auto& crtc_info : active_crtcs_) {
    result.emplace_back(crtc_info.x, crtc_info.y, crtc_info.width,
                        crtc_info.height);
  }
  return result;
}

void X11CrtcResizer::AddCrtcFromReply(
    x11::RandR::Crtc crtc,
    const x11::RandR::GetCrtcInfoReply& reply) {
  active_crtcs_.emplace_back(crtc, reply.x, reply.y, reply.width, reply.height,
                             reply.mode, reply.rotation, reply.outputs);
}

bool X11CrtcResizer::LayoutIsVertical() const {
  if (active_crtcs_.size() <= 1) {
    return false;
  }

  // For simplicity, just pick 2 CRTCs arbitrarily.
  auto iter1 = active_crtcs_.begin();
  auto iter2 = std::next(iter1);

  // The cases:
  // --[---]--------
  // --------[---]--
  // and:
  // --------[---]--
  // --[---]--------
  // are not vertically stacked. The case where the CRTCs are exactly touching
  // is also not vertically stacked, because it comes from a horizontal
  // packing of CRTCs:
  // --[---]-------
  // -------[---]--
  // All other cases have overlapping projections so they are considered
  // vertically stacked.
  auto left1 = iter1->x;
  auto right1 = iter1->x + iter1->width;
  auto left2 = iter2->x;
  auto right2 = iter2->x + iter2->width;

  return right1 > left2 && right2 > left1;
}

void X11CrtcResizer::PackVertically(const gfx::Size& new_size,
                                    x11::RandR::Crtc resized_crtc) {
  // Before applying the new size, test for any current alignments to
  // decide which alignment should be preserved, if any.
  DCHECK(!active_crtcs_.empty());
  bool is_left_aligned = true;
  bool is_middle_aligned = true;
  bool is_right_aligned = true;
  auto first_crtc_left = active_crtcs_.front().x;
  auto first_crtc_middle = first_crtc_left + active_crtcs_.front().width / 2;
  auto first_crtc_right = first_crtc_left + active_crtcs_.front().width;
  for (const auto& crtc_info : active_crtcs_) {
    auto crtc_left = crtc_info.x;
    auto crtc_middle = crtc_info.x + crtc_info.width / 2;
    auto crtc_right = crtc_info.x + crtc_info.width;
    if (crtc_left != first_crtc_left) {
      is_left_aligned = false;
    }
    if (abs(crtc_middle - first_crtc_middle) > 1) {
      is_middle_aligned = false;
    }
    if (crtc_right != first_crtc_right) {
      is_right_aligned = false;
    }
  }

  // Apply the new size.
  for (auto& crtc_info : active_crtcs_) {
    if (crtc_info.crtc == resized_crtc) {
      crtc_info.width = new_size.width();
      crtc_info.height = new_size.height();
      break;
    }
  }

  // Sort vertically before packing.
  std::ranges::sort(active_crtcs_, std::less<>(), &CrtcInfo::y);

  // Pack the CRTCs by setting their y-offsets. If necessary, change the
  // x-offset for right-alignment.
  int current_y = 0;
  for (auto& crtc_info : active_crtcs_) {
    crtc_info.y = current_y;
    current_y += crtc_info.height;

    // Place all monitors, respecting any alignment preference. If there are
    // multiple possible alignments, prioritize left, then right, then middle.
    // TODO(crbug.com/40225767): Implement a more sophisticated algorithm that
    // tries to preserve pairwise alignment. It is not enough to leave the
    // x-offsets unchanged here - this tends to result in the monitors being
    // arranged roughly diagonally, wasting lots of space. Some amount of
    // horizontal compression is needed to prevent this from happening.
    if (is_left_aligned) {
      crtc_info.x = 0;
    } else if (is_right_aligned) {
      crtc_info.x = -crtc_info.width;
    } else if (is_middle_aligned) {
      crtc_info.x = -crtc_info.width / 2;
    } else {
      // The current implementation left-aligns the CRTCs if no other
      // alignment is detected.
      // TODO(crbug.com/40225767): A future enhancement may be to detect and
      // report one of {left, middle, right, none}. The "none" case (for
      // vertical and horizontal layouts) could be treated as a
      // client-controlled layout, where the host does not attempt any
      // repositioning. In this case, the host could still support
      // resize-to-fit, but in a simplified way - resize would be allowed
      // whenever it creates no overlaps.
      crtc_info.x = 0;
    }
  }
}

void X11CrtcResizer::PackHorizontally(const gfx::Size& new_size,
                                      x11::RandR::Crtc resized_crtc) {
  gfx::Size new_size_transposed(new_size.height(), new_size.width());
  Transpose();
  PackVertically(new_size_transposed, resized_crtc);
  Transpose();
}

void X11CrtcResizer::Transpose() {
  for (auto& crtc_info : active_crtcs_) {
    std::swap(crtc_info.x, crtc_info.y);
    std::swap(crtc_info.width, crtc_info.height);
  }
}

void X11CrtcResizer::MoveWindow(x11::Window window,
                                const x11::GetWindowAttributesReply& attributes,
                                gfx::Point top_left) {
  if (!attributes.override_redirect) {
    // Try to locate the original window which was re-parented by the
    // window-manager.
    auto app_window = FindAppWindow(window, attributes);
    if (app_window != x11::Window::None) {
      window = app_window;
    }
  }

  connection_->ConfigureWindow({
      .window = window,
      .x = top_left.x(),
      .y = top_left.y(),
  });
}

x11::Window X11CrtcResizer::FindAppWindow(
    x11::Window window,
    const x11::GetWindowAttributesReply& attributes) {
  // Only descend into visible windows.
  if (attributes.map_state != x11::MapState::Viewable) {
    return x11::Window::None;
  }

  // If the window itself has the WM_STATE property, return it. Otherwise,
  // descend into the window's children recursively.
  auto property_reply = connection_
                            ->GetProperty({
                                .window = window,
                                .property = wm_state_atom_,
                                .long_length = 1,
                            })
                            .Sync();
  if (!property_reply) {
    return x11::Window::None;
  }
  if (property_reply->value_len != 0) {
    // Found window with WM_STATE property.
    return window;
  }

  // Descend into the children of |window| in stacking order. See for example:
  // Find_Client_In_Children() at
  // https://github.com/freedesktop/xorg-xwininfo/blob/master/clientwin.c
  auto query_response = connection_->QueryTree({window}).Sync();
  if (!query_response) {
    return x11::Window::None;
  }

  for (const auto& child_window : base::Reversed(query_response->children)) {
    auto attributes_response =
        connection_->GetWindowAttributes({child_window}).Sync();
    if (!attributes_response) {
      return x11::Window::None;
    }
    auto found_window = FindAppWindow(child_window, *attributes_response.reply);
    if (found_window != x11::Window::None) {
      return found_window;
    }
  }
  return x11::Window::None;
}

}  // namespace x11
