// Copyright 2023 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/geometry_cache.h"

#include <memory>

#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/x/connection.h"
#include "ui/gfx/x/event.h"
#include "ui/gfx/x/xproto.h"

namespace x11 {

GeometryCache::GeometryCache(Connection* connection,
                             Window window,
                             BoundsChangedCallback bounds_changed_callback)
    : connection_(connection),
      window_(window),
      bounds_changed_callback_(bounds_changed_callback) {
  scoped_observation_.Observe(connection_);
  window_events_ =
      connection_->ScopedSelectEvent(window_, EventMask::StructureNotify);

  parent_future_ = connection_->QueryTree(window_);
  parent_future_.OnResponse(base::BindOnce(&GeometryCache::OnQueryTreeResponse,
                                           weak_ptr_factory_.GetWeakPtr()));
  geometry_future_ = connection_->GetGeometry(window_);
  geometry_future_.OnResponse(base::BindOnce(
      &GeometryCache::OnGetGeometryResponse, weak_ptr_factory_.GetWeakPtr()));
}

GeometryCache::~GeometryCache() = default;

gfx::Rect GeometryCache::GetBoundsPx() {
  auto get_local_geometry = [this]() {
    if (have_geometry_) {
      return geometry_;
    }
    if (auto response = geometry_future_.Peek()) {
      return response ? gfx::Rect(response->x, response->y, response->width,
                                  response->height)
                      : gfx::Rect();
    }
    return geometry_;
  };

  if (!have_parent_) {
    auto response = parent_future_.Peek();
    if (!response) {
      return get_local_geometry();
    }
    Window parent_window = response ? response->parent : Window::None;
    if (parent_window == Window::None) {
      parent_.reset();
      return get_local_geometry();
    }
    if (!parent_ || parent_->window_ != parent_window) {
      parent_ = std::make_unique<GeometryCache>(
          connection_, parent_window,
          base::BindRepeating(&GeometryCache::OnParentGeometryChanged,
                              weak_ptr_factory_.GetWeakPtr()));
    }
  }

  gfx::Rect geometry = get_local_geometry();
  if (!parent_) {
    return geometry;
  }
  auto parent_bounds = parent_->GetBoundsPx();
  gfx::Vector2d offset(parent_bounds.x(), parent_bounds.y());
  return geometry + offset;
}

void GeometryCache::OnQueryTreeResponse(QueryTreeResponse response) {
  if (have_parent_) {
    return;
  }
  OnParentChanged(response ? response->parent : Window::None,
                  geometry_.origin());
}

void GeometryCache::OnGetGeometryResponse(GetGeometryResponse response) {
  if (have_geometry_) {
    return;
  }
  OnGeometryChanged(response ? gfx::Rect(response->x, response->y,
                                         response->width, response->height)
                             : gfx::Rect());
}

void GeometryCache::OnParentChanged(Window parent, const gfx::Point& position) {
  have_parent_ = true;
  if (parent == Window::None) {
    parent_.reset();
  } else if (!parent_ || parent_->window_ != parent) {
    parent_ = std::make_unique<GeometryCache>(
        connection_, parent,
        base::BindRepeating(&GeometryCache::OnParentGeometryChanged,
                            weak_ptr_factory_.GetWeakPtr()));
  }

  geometry_.set_origin(position);

  NotifyGeometryChanged();
}

void GeometryCache::OnGeometryChanged(const gfx::Rect& geometry) {
  have_geometry_ = true;
  geometry_ = geometry;

  NotifyGeometryChanged();
}

void GeometryCache::OnPositionChanged(const gfx::Point& origin) {
  geometry_.set_origin(origin);
  NotifyGeometryChanged();
}

bool GeometryCache::Ready() const {
  return have_geometry_ && have_parent_ && (!parent_ || parent_->Ready());
}

void GeometryCache::OnParentGeometryChanged(
    const std::optional<gfx::Rect>& old_parent_bounds,
    const gfx::Rect& new_parent_bounds) {
  NotifyGeometryChanged();
}

void GeometryCache::NotifyGeometryChanged() {
  if (!Ready()) {
    return;
  }

  auto geometry = GetBoundsPx();
  if (last_notified_geometry_ == geometry) {
    return;
  }

  auto old_geometry = last_notified_geometry_;
  last_notified_geometry_ = geometry;
  bounds_changed_callback_.Run(old_geometry, geometry);
}

void GeometryCache::OnEvent(const Event& xevent) {
  // Ignore client events.
  if (xevent.send_event()) {
    return;
  }

  if (auto* configure = xevent.As<ConfigureNotifyEvent>()) {
    if (configure->window == window_) {
      OnGeometryChanged(gfx::Rect(configure->x, configure->y, configure->width,
                                  configure->height));
    }
  } else if (auto* reparent = xevent.As<ReparentNotifyEvent>()) {
    if (reparent->window == window_) {
      OnParentChanged(reparent->parent, gfx::Point(reparent->x, reparent->y));
    }
  } else if (auto* gravity = xevent.As<GravityNotifyEvent>()) {
    if (gravity->window == window_) {
      OnPositionChanged(gfx::Point(gravity->x, gravity->y));
    }
  }
}

}  // namespace x11
