// 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 "chrome/browser/ui/views/frame/browser_desktop_window_tree_host_linux.h"

#include <optional>
#include <utility>

#include "base/no_destructor.h"
#include "build/build_config.h"
#include "chrome/browser/ui/browser_init_state.h"
#include "chrome/browser/ui/views/frame/browser_frame_view_linux.h"
#include "chrome/browser/ui/views/frame/browser_native_widget_aura_linux.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/tabs/dragging/tab_drag_controller.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_set.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom-shared.h"
#include "ui/base/mojom/window_show_state.mojom.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/linux/linux_ui.h"
#include "ui/ozone/public/ozone_platform.h"
#include "ui/platform_window/extensions/x11_extension.h"
#include "ui/platform_window/platform_window_init_properties.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/frame_view_utils_linux.h"

namespace {

#if BUILDFLAG(USE_DBUS)
bool ShouldCreateGlobalMenuBar() {
  return ui::OzonePlatform::GetInstance()
      ->GetPlatformRuntimeProperties()
      .supports_global_application_menus;
}
#endif

absl::flat_hash_set<std::string>& SentStartupIds() {
  static base::NoDestructor<absl::flat_hash_set<std::string>> sent_startup_ids;
  return *sent_startup_ids;
}

// Returns the event source for the active tab drag session.
std::optional<ui::mojom::DragEventSource> GetCurrentTabDragEventSource() {
  if (auto* source_context = TabDragController::GetSourceContext()) {
    if (auto* drag_controller = source_context->GetDragController()) {
      return drag_controller->event_source();
    }
  }
  return std::nullopt;
}

bool IsShowingFrame(bool use_custom_frame,
                    ui::PlatformWindowState window_state) {
  return use_custom_frame &&
         window_state != ui::PlatformWindowState::kMaximized &&
         window_state != ui::PlatformWindowState::kMinimized &&
         window_state != ui::PlatformWindowState::kFullScreen;
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// BrowserDesktopWindowTreeHostLinux, public:

BrowserDesktopWindowTreeHostLinux::BrowserDesktopWindowTreeHostLinux(
    views::internal::NativeWidgetDelegate* native_widget_delegate,
    views::DesktopNativeWidgetAura* desktop_native_widget_aura,
    BrowserView* browser_view,
    BrowserWidget* browser_widget)
    : DesktopWindowTreeHostLinux(native_widget_delegate,
                                 desktop_native_widget_aura),
      browser_view_(browser_view),
      browser_widget_(browser_widget) {
  native_widget_ = static_cast<BrowserNativeWidgetAuraLinux*>(
      browser_widget->browser_native_widget());
  native_widget_->set_host(this);

  browser_widget->set_frame_type(native_widget_->UseCustomFrame()
                                     ? views::Widget::FrameType::kForceCustom
                                     : views::Widget::FrameType::kForceNative);

  if (auto* linux_ui = ui::LinuxUi::instance()) {
    scale_observation_.Observe(linux_ui);
  }
}

BrowserDesktopWindowTreeHostLinux::~BrowserDesktopWindowTreeHostLinux() {
  native_widget_->set_host(nullptr);
}

////////////////////////////////////////////////////////////////////////////////
// BrowserDesktopWindowTreeHostLinux,
//     BrowserDesktopWindowTreeHost implementation:

void BrowserDesktopWindowTreeHostLinux::AddAdditionalInitProperties(
    const views::Widget::InitParams& params,
    ui::PlatformWindowInitProperties* properties) {
  views::DesktopWindowTreeHostLinux::AddAdditionalInitProperties(params,
                                                                 properties);
  auto* profile = browser_view_->browser()->GetProfile();
  const auto* linux_ui_theme = ui::LinuxUiTheme::GetForProfile(profile);
  properties->prefer_dark_theme =
      linux_ui_theme && linux_ui_theme->PreferDarkTheme();
}

views::DesktopWindowTreeHost*
BrowserDesktopWindowTreeHostLinux::AsDesktopWindowTreeHost() {
  return this;
}

bool BrowserDesktopWindowTreeHostLinux::UsesNativeSystemMenu() const {
  return false;
}

void BrowserDesktopWindowTreeHostLinux::FrameTypeChanged() {
  DesktopWindowTreeHostPlatform::FrameTypeChanged();
  UpdateFrameHints();
}

void BrowserDesktopWindowTreeHostLinux::TabDraggingKindChanged(
    TabDragKind tab_drag_kind) {
  CHECK(browser_widget_);
  CHECK(browser_view_);
  // If there's no tabs left, the browser window is about to close, so don't
  // call SetOverrideRedirect() to prevent the window from flashing.
  if (!browser_view_->browser()->GetTabStripModel()->count()) {
    return;
  }

  bool was_dragging_window =
      browser_widget_->tab_drag_kind() == TabDragKind::kAllTabs;
  bool is_dragging_window = tab_drag_kind == TabDragKind::kAllTabs;
  if (is_dragging_window != was_dragging_window) {
    browser_widget_->SetBypassWindowManager(is_dragging_window);
  }

  if (tab_drag_kind != TabDragKind::kNone) {
    if (auto event_source = GetCurrentTabDragEventSource()) {
      views::Widget::MoveLoopSource move_loop_source =
          (*event_source == ui::mojom::DragEventSource::kTouch)
              ? views::Widget::MoveLoopSource::kTouch
              : views::Widget::MoveLoopSource::kMouse;
      browser_widget_->PrepareForMoveLoop(move_loop_source);
    }
  }
}

bool BrowserDesktopWindowTreeHostLinux::SupportsClientFrameShadow() const {
  return platform_window()->CanSetDecorationInsets() &&
         views::Widget::IsWindowCompositingSupported();
}

void BrowserDesktopWindowTreeHostLinux::UpdateFrameHints() {
  if (!browser_widget_) {
    return;
  }

  auto* window = platform_window();
  auto window_state = window->GetPlatformWindowState();
  float scale = device_scale_factor();
  auto* view = static_cast<BrowserFrameView*>(browser_widget_->GetFrameView());
  const gfx::Size widget_size =
      view->GetWidget()->GetWindowBoundsInScreen().size();

  if (SupportsClientFrameShadow()) {
    auto insets = CalculateInsetsInDIP(window_state);
    if (insets.IsEmpty()) {
      window->SetInputRegion(std::nullopt);
    } else {
      gfx::Rect input_bounds(widget_size);
      input_bounds.Inset(insets - view->GetInputInsets());
      input_bounds = gfx::ScaleToEnclosingRect(input_bounds, scale);
      window->SetInputRegion(
          std::optional<std::vector<gfx::Rect>>({input_bounds}));
    }
  }

  if (ui::OzonePlatform::GetInstance()->IsWindowCompositingSupported()) {
    // Set the opaque region.
    std::vector<gfx::Rect> opaque_region;
    if (IsShowingFrame(native_widget_->UseCustomFrame(), window_state)) {
      opaque_region =
          views::GetRestoredOpaqueRegion(view->GetRestoredClipRegion(), scale,
                                         view->GetTranslucentTopAreaHeight());
    } else {
      // The entire window except for the translucent top is opaque.
      gfx::Rect opaque_region_dip(widget_size);
      gfx::Insets insets;
      insets.set_top(view->GetTranslucentTopAreaHeight());
      opaque_region_dip.Inset(insets);
      opaque_region.push_back(
          gfx::ScaleToEnclosingRect(opaque_region_dip, scale));
    }
    window->SetOpaqueRegion(opaque_region);
  }

  SizeConstraintsChanged();
}

void BrowserDesktopWindowTreeHostLinux::ClientDestroyedWidget() {
#if BUILDFLAG(USE_DBUS)
  dbus_appmenu_.reset();
#endif
  browser_widget_ = nullptr;
  browser_view_ = nullptr;
  DesktopWindowTreeHostLinux::ClientDestroyedWidget();
}

////////////////////////////////////////////////////////////////////////////////
// BrowserDesktopWindowTreeHostLinux,
//     DesktopWindowTreeHostLinuxImpl implementation:

void BrowserDesktopWindowTreeHostLinux::Init(
    const views::Widget::InitParams& params) {
  DesktopWindowTreeHostLinux::Init(std::move(params));

#if BUILDFLAG(USE_DBUS)
  // We have now created our backing window. We now need to (possibly)
  // alert the desktop environment that there's a menu bar attached to it.
  if (ShouldCreateGlobalMenuBar()) {
    dbus_appmenu_ = std::make_unique<DbusAppmenu>(
        browser_view_, platform_window(), GetAcceleratedWidget());
  }
#endif
}

void BrowserDesktopWindowTreeHostLinux::OnWidgetInitDone() {
  DesktopWindowTreeHostLinux::OnWidgetInitDone();

  UpdateFrameHints();
}

void BrowserDesktopWindowTreeHostLinux::CloseNow() {
#if BUILDFLAG(USE_DBUS)
  dbus_appmenu_.reset();
#endif
  DesktopWindowTreeHostLinux::CloseNow();
}

void BrowserDesktopWindowTreeHostLinux::Show(
    ui::mojom::WindowShowState show_state,
    const gfx::Rect& restore_bounds) {
  CHECK(browser_view_);
  DesktopWindowTreeHostLinux::Show(show_state, restore_bounds);

  const std::string& startup_id =
      BrowserInitState::From(browser_view_->browser())
          ->create_params()
          .startup_id;
  if (!startup_id.empty() && !SentStartupIds().contains(startup_id)) {
    platform_window()->NotifyStartupComplete(startup_id);
    SentStartupIds().insert(startup_id);
  }
}

bool BrowserDesktopWindowTreeHostLinux::IsOverrideRedirect(
    const ui::X11Extension& x11_extension) const {
  return (browser_widget_ &&
          browser_widget_->tab_drag_kind() == TabDragKind::kAllTabs) &&
         x11_extension.IsWmTiling() && x11_extension.CanResetOverrideRedirect();
}

gfx::Insets BrowserDesktopWindowTreeHostLinux::CalculateInsetsInDIP(
    ui::PlatformWindowState window_state) const {
  // If we are not showing frame, the insets should be zero.
  if (!browser_widget_ ||
      !IsShowingFrame(native_widget_->UseCustomFrame(), window_state)) {
    return gfx::Insets();
  }

  return static_cast<BrowserFrameView*>(browser_widget_->GetFrameView())
      ->RestoredMirroredFrameBorderInsets();
}

void BrowserDesktopWindowTreeHostLinux::OnWindowStateChanged(
    ui::PlatformWindowState old_state,
    ui::PlatformWindowState new_state) {
  if (!browser_view_) {
    return;
  }

  DesktopWindowTreeHostLinux::OnWindowStateChanged(old_state, new_state);

  bool fullscreen_changed = new_state == ui::PlatformWindowState::kFullScreen ||
                            old_state == ui::PlatformWindowState::kFullScreen;
  if (old_state != new_state && fullscreen_changed) {
    // If the browser view initiated this state change,
    // BrowserView::ProcessFullscreen will no-op, so this call is harmless.
    browser_view_->FullscreenStateChanging();

    // In sync fullscreen window state mode, the FullscreenStateChanged()
    // is called just after requesting to set fullscreen property
    // to the platform, so not to call here to avoid duplicated invocation.
    if (base::FeatureList::IsEnabled(features::kAsyncFullscreenWindowState)) {
      browser_view_->FullscreenStateChanged();
    }
  }

  UpdateFrameHints();
}

void BrowserDesktopWindowTreeHostLinux::OnWindowTiledStateChanged(
    ui::WindowTiledEdges new_tiled_edges) {
  if (!browser_widget_) {
    return;
  }

  browser_widget_->set_tiled(new_tiled_edges.top || new_tiled_edges.left ||
                             new_tiled_edges.bottom || new_tiled_edges.right);
  UpdateFrameHints();
  if (SupportsClientFrameShadow()) {
    // Trigger a re-layout as the insets will change even if the bounds don't.
    ScheduleRelayout();
    if (GetWidget()->non_client_view()) {
      // This is needed for the decorated regions, borders etc. to be repainted.
      GetWidget()->non_client_view()->SchedulePaint();
    }
  }
}

void BrowserDesktopWindowTreeHostLinux::OnNativeThemeUpdated(
    ui::NativeTheme* observed_theme) {
  views::DesktopWindowTreeHostLinux::OnNativeThemeUpdated(observed_theme);
  UpdateFrameHints();
}

void BrowserDesktopWindowTreeHostLinux::OnDeviceScaleFactorChanged() {
  UpdateFrameHints();
}

////////////////////////////////////////////////////////////////////////////////
// BrowserDesktopWindowTreeHost, public:

// static
BrowserDesktopWindowTreeHost*
BrowserDesktopWindowTreeHost::CreateBrowserDesktopWindowTreeHost(
    views::internal::NativeWidgetDelegate* native_widget_delegate,
    views::DesktopNativeWidgetAura* desktop_native_widget_aura,
    BrowserView* browser_view,
    BrowserWidget* browser_widget) {
  return new BrowserDesktopWindowTreeHostLinux(native_widget_delegate,
                                               desktop_native_widget_aura,
                                               browser_view, browser_widget);
}
