// 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 "components/constrained_window/constrained_window_views.h"

#include <algorithm>
#include <memory>

#include "base/check_op.h"
#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/no_destructor.h"
#include "base/scoped_observation.h"
#include "build/build_config.h"
#include "components/constrained_window/constrained_window_views_client.h"
#include "components/guest_view/browser/guest_view_base.h"
#include "components/web_modal/web_contents_modal_dialog_host.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "components/web_modal/web_contents_modal_dialog_manager_delegate.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/native_ui_types.h"
#include "ui/views/bubble/bubble_dialog_model_host.h"
#include "ui/views/widget/native_widget.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_observer.h"
#include "ui/views/window/dialog_delegate.h"
#include "url/gurl.h"

#if BUILDFLAG(IS_OZONE)
#include "ui/ozone/public/ozone_platform.h"
#endif

using web_modal::ModalDialogHost;
using web_modal::ModalDialogHostObserver;

DEFINE_UI_CLASS_PROPERTY_TYPE(ModalDialogHostObserver*)
DEFINE_OWNED_UI_CLASS_PROPERTY_KEY(ModalDialogHostObserver,
                                   kModalDialogHostObserverKey)

namespace constrained_window {

const void* kConstrainedWindowWidgetIdentifier = "ConstrainedWindowWidget";

namespace {

// Storage access for the currently active ConstrainedWindowViewsClient.
std::unique_ptr<ConstrainedWindowViewsClient>& CurrentBrowserModalClient() {
  static base::NoDestructor<std::unique_ptr<ConstrainedWindowViewsClient>>
      client;
  return *client;
}

// Closes the dialog widget when the modal host has been destroyed and applies
// positioning changes from the ModalDialogHost to the Widget.
class ModalDialogHostObserverViews : public ModalDialogHostObserver {
 public:
  ModalDialogHostObserverViews(ModalDialogHost* host,
                               views::Widget* dialog_widget,
                               bool auto_update_position)
      : host_(host),
        dialog_widget_(dialog_widget),
        auto_update_position_(auto_update_position) {
    CHECK(host_);
    CHECK(dialog_widget_);
    modal_dialog_host_observation_.Observe(host);
  }
  ModalDialogHostObserverViews(const ModalDialogHostObserverViews&) = delete;
  ModalDialogHostObserverViews& operator=(const ModalDialogHostObserverViews&) =
      delete;
  ~ModalDialogHostObserverViews() override = default;

  // ModalDialogHostObserver:
  void OnPositionRequiresUpdate() override {
    if (auto_update_position_) {
      CHECK(host_);
      UpdateWidgetModalDialogPosition(dialog_widget_, host_);
    }
  }
  void OnHostDestroying() override {
    // Synchronously close the dialog widget to avoid dangling references to the
    // host.
    modal_dialog_host_observation_.Reset();
    host_ = nullptr;
    dialog_widget_->CloseNow();
  }

 private:
  // The modal host for the widget that owns this observer.
  raw_ptr<ModalDialogHost> host_;

  // Owns this observer.
  raw_ptr<views::Widget> dialog_widget_;

  // Applies positioning changes from the ModalDialogHost to the Widget if true.
  const bool auto_update_position_;

  base::ScopedObservation<ModalDialogHost, ModalDialogHostObserver>
      modal_dialog_host_observation_{this};
};

gfx::Rect GetModalDialogBounds(views::Widget* widget,
                               web_modal::ModalDialogHost* dialog_host,
                               const gfx::Size& size) {
  // |host_view| will be nullptr with CEF windowless rendering.
  auto host_view = dialog_host->GetHostView();
  views::Widget* host_widget =
      host_view ? views::Widget::GetWidgetForNativeView(host_view) : nullptr;

  // If the host view is not backed by a Views::Widget, just update the widget
  // size. This can happen on MacViews under the Cocoa browser where the window
  // modal dialogs are displayed as sheets, and their position is managed by a
  // ConstrainedWindowSheetController instance.
  if (!host_widget) {
    return gfx::Rect(dialog_host->GetDialogPosition(size), size);
  }

  gfx::Point position = dialog_host->GetDialogPosition(size);
  // Align the first row of pixels inside the border. This is the apparent top
  // of the dialog.
  position.set_y(position.y() -
                 widget->non_client_view()->frame_view()->GetInsets().top());

  if (widget->is_top_level() && SupportsGlobalScreenCoordinates()) {
    position += host_widget->GetClientAreaBoundsInScreen().OffsetFromOrigin();
    // If the dialog extends partially off any display, clamp its position to
    // be fully visible within that display. If the dialog doesn't intersect
    // with any display clamp its position to be fully on the nearest display.
    gfx::Rect display_rect = gfx::Rect(position, size);
    const display::Display display =
        display::Screen::Get()->GetDisplayNearestView(
            dialog_host->GetHostView());
    const gfx::Rect work_area = display.work_area();
    if (!work_area.Contains(display_rect))
      display_rect.AdjustToFit(work_area);
    position = display_rect.origin();
  }

  return gfx::Rect(position, size);
}

void UpdateModalDialogPosition(views::Widget* widget,
                               web_modal::ModalDialogHost* dialog_host,
                               const gfx::Size& size) {
  // Do not forcibly update the dialog widget position if it is being dragged.
  if (widget->HasCapture()) {
    return;
  }

  // |host_view| will be nullptr with CEF windowless rendering.
  auto host_view = dialog_host->GetHostView();
  views::Widget* host_widget =
      host_view ? views::Widget::GetWidgetForNativeView(host_view) : nullptr;

  // If the host view is not backed by a Views::Widget, just update the widget
  // size. This can happen on MacViews under the Cocoa browser where the window
  // modal dialogs are displayed as sheets, and their position is managed by a
  // ConstrainedWindowSheetController instance.
  if (!host_widget) {
#if BUILDFLAG(IS_MAC)
    widget->SetSize(size);
#elif BUILDFLAG(IS_POSIX)
    // Set the bounds here instead of relying on the default behavior of
    // DesktopWindowTreeHostPlatform::CenterWindow which incorrectly centers
    // the window on the screen.
    widget->SetBounds(gfx::Rect(dialog_host->GetDialogPosition(size), size));
#endif
    return;
  }

  widget->SetBounds(GetModalDialogBounds(widget, dialog_host, size));
}

void ConfigureDesiredBoundsDelegate(views::WidgetDelegate* dialog_delegate,
                                    web_modal::ModalDialogHost* dialog_host) {
  views::Widget* widget = dialog_delegate->GetWidget();
  CHECK(widget)
      << "SetDesiredBoundsDelegate() must be called after creating the widget.";
  dialog_delegate->set_desired_bounds_delegate(base::BindRepeating(
      [](views::Widget* widget,
         web_modal::ModalDialogHost* dialog_host) -> gfx::Rect {
        return GetModalDialogBounds(
            widget, dialog_host, widget->GetRootView()->GetPreferredSize({}));
      },
      widget, dialog_host));
}

}  // namespace

class BrowserModalHelper {
 public:
  static views::Widget* Show(std::unique_ptr<ui::DialogModel> dialog_model,
                             gfx::NativeWindow parent) {
    gfx::NativeView parent_view =
        parent ? CurrentBrowserModalClient()->GetDialogHostView(parent) :
                 gfx::NativeView();
    // Use with CEF windowless rendering.
    gfx::AcceleratedWidget parent_widget =
        parent ? CurrentBrowserModalClient()->GetModalDialogHost(parent)->
            GetAcceleratedWidget() : gfx::kNullAcceleratedWidget;

    // TODO(crbug.com/41493925): Remove will_use_custom_frame once native frame
    // dialogs support autosize.
    bool will_use_custom_frame = views::DialogDelegate::CanSupportCustomFrame(
        parent_view, parent_widget);
    auto dialog = views::BubbleDialogModelHost::CreateModal(
        std::move(dialog_model), ui::mojom::ModalType::kWindow,
        will_use_custom_frame);
    dialog->SetOwnedByWidget(views::WidgetDelegate::OwnedByWidgetPassKey());
    auto* widget = constrained_window::CreateBrowserModalDialogViews(
        std::move(dialog), parent);
    CHECK_EQ(widget->widget_delegate()->AsDialogDelegate()->use_custom_frame(),
             will_use_custom_frame);
    widget->Show();
    return widget;
  }
};

// static
void SetConstrainedWindowViewsClient(
    std::unique_ptr<ConstrainedWindowViewsClient> new_client) {
  CurrentBrowserModalClient() = std::move(new_client);
}

void UpdateWebContentsModalDialogPosition(
    views::Widget* widget,
    web_modal::WebContentsModalDialogHost* dialog_host) {
  gfx::Size size = widget->GetRootView()->GetPreferredSize({});
  gfx::Size max_size = dialog_host->GetMaximumDialogSize();
  // Enlarge the max size by the top border, as the dialog will be shifted
  // outside the area specified by the dialog host by this amount later.
  max_size.Enlarge(0,
                   widget->non_client_view()->frame_view()->GetInsets().top());
  size.SetToMin(max_size);
  UpdateModalDialogPosition(widget, dialog_host, size);
}

void UpdateWidgetModalDialogPosition(views::Widget* widget,
                                     web_modal::ModalDialogHost* dialog_host) {
  UpdateModalDialogPosition(widget, dialog_host,
                            widget->GetRootView()->GetPreferredSize({}));
}

content::WebContents* GetTopLevelWebContents(
    content::WebContents* initiator_web_contents) {
  // TODO(mcnee): While calling both `GetResponsibleWebContents` and
  // `GetTopLevelWebContents` appears redundant, there appears to still be cases
  // where users of guest view are not initializing the guest WebContents
  // properly, causing GetResponsibleWebContents to break. See
  // https://crbug.com/1325850
  // The order of composing these methods is arbitrary.
  return guest_view::GuestViewBase::GetTopLevelWebContents(
      initiator_web_contents->GetResponsibleWebContents());
}

views::Widget* ShowWebModalDialogViews(
    views::WidgetDelegate* dialog,
    content::WebContents* initiator_web_contents) {
  // For embedded WebContents, use the embedder's WebContents for constrained
  // window.
  content::WebContents* web_contents =
      GetTopLevelWebContents(initiator_web_contents);
  views::Widget* widget = CreateWebModalDialogViews(dialog, web_contents);
  ShowModalDialog(widget->GetNativeWindow(), web_contents);
  return widget;
}

std::unique_ptr<views::Widget> ShowWebModalDialogViewsOwned(
    views::WidgetDelegate* dialog,
    content::WebContents* initiator_web_contents,
    views::Widget::InitParams::Ownership expected_ownership) {
  views::Widget* widget =
      ShowWebModalDialogViews(dialog, initiator_web_contents);
  CHECK_EQ(widget->ownership(), expected_ownership);
  return base::WrapUnique<views::Widget>(widget);
}

// TODO(crbug.com/353174863): This currently creates a constrained dialog
// assumed to be constrained to the initial window hosting `web_contents`. This
// should be updated to follow `web_contents` as it is moved across windows.
views::Widget* CreateWebModalDialogViews(views::WidgetDelegate* dialog,
                                         content::WebContents* web_contents) {
  DCHECK_EQ(ui::mojom::ModalType::kChild, dialog->GetModalType());
  web_modal::WebContentsModalDialogManager* manager =
      web_modal::WebContentsModalDialogManager::FromWebContents(web_contents);
  web_modal::ModalDialogHost* const dialog_host =
      manager->delegate()->GetWebContentsModalDialogHost(web_contents);
  CHECK(dialog_host);

  // Use desktop widget so that it is not constrained by the boundary of the
  // host window.
  dialog->set_use_desktop_widget_override(
      !dialog_host->ShouldConstrainDialogBoundsByHost());

  views::Widget* widget = views::DialogDelegate::CreateDialogWidget(
      dialog, gfx::NativeWindow(), dialog_host->GetHostView());
  std::unique_ptr<ModalDialogHostObserver> observer =
      std::make_unique<ModalDialogHostObserverViews>(
          dialog_host, widget, /*auto_update_position=*/false);
  widget->SetProperty(kModalDialogHostObserverKey, std::move(observer));
  ConfigureDesiredBoundsDelegate(dialog, dialog_host);
  widget->SetNativeWindowProperty(
      views::kWidgetIdentifierKey,
      const_cast<void*>(kConstrainedWindowWidgetIdentifier));

  return widget;
}

views::Widget* CreateBrowserModalDialogViews(
    std::unique_ptr<views::DialogDelegate> dialog,
    gfx::NativeWindow parent) {
  return CreateBrowserModalDialogViews(dialog.release(), parent);
}

views::Widget* CreateBrowserModalDialogViews(views::DialogDelegate* dialog,
                                             gfx::NativeWindow parent) {
  DCHECK_NE(ui::mojom::ModalType::kChild, dialog->GetModalType());
  DCHECK_NE(ui::mojom::ModalType::kNone, dialog->GetModalType());
  DCHECK(!parent || CurrentBrowserModalClient());

  gfx::NativeView parent_view =
      parent ? CurrentBrowserModalClient()->GetDialogHostView(parent)
             : gfx::NativeView();
  // Use with CEF windowless rendering.
  gfx::AcceleratedWidget parent_widget =
      parent ? CurrentBrowserModalClient()->GetModalDialogHost(parent)->
          GetAcceleratedWidget() : gfx::kNullAcceleratedWidget;
  views::Widget* widget = views::DialogDelegate::CreateDialogWidget(
      dialog, gfx::NativeWindow(), parent_view, parent_widget);
  widget->SetNativeWindowProperty(
      views::kWidgetIdentifierKey,
      const_cast<void*>(kConstrainedWindowWidgetIdentifier));

  bool requires_positioning = dialog->use_custom_frame();

#if BUILDFLAG(IS_APPLE)
  // On Mac, window modal dialogs are displayed as sheets, so their position is
  // managed by the parent window.
  requires_positioning = false;
#endif

  if (!requires_positioning)
    return widget;

  ModalDialogHost* host =
      CurrentBrowserModalClient()->GetModalDialogHost(parent);
  if (host) {
    DCHECK_EQ(parent_view, host->GetHostView());
    std::unique_ptr<ModalDialogHostObserver> observer =
        std::make_unique<ModalDialogHostObserverViews>(
            host, widget, /*auto_update_position=*/true);
    widget->SetProperty(kModalDialogHostObserverKey, std::move(observer));
    widget->GetProperty(kModalDialogHostObserverKey)
        ->OnPositionRequiresUpdate();
    ConfigureDesiredBoundsDelegate(dialog, host);
  }

  return widget;
}

views::Widget* ShowBrowserModal(std::unique_ptr<ui::DialogModel> dialog_model,
                                gfx::NativeWindow parent) {
  return BrowserModalHelper::Show(std::move(dialog_model), parent);
}

views::Widget* ShowWebModal(std::unique_ptr<ui::DialogModel> dialog_model,
                            content::WebContents* web_contents) {
  return constrained_window::ShowWebModalDialogViews(
      views::BubbleDialogModelHost::CreateModal(std::move(dialog_model),
                                                ui::mojom::ModalType::kChild)
          .release(),
      web_contents);
}

bool SupportsGlobalScreenCoordinates() {
#if !BUILDFLAG(IS_OZONE)
  return true;
#else
  return ui::OzonePlatform::GetInstance()
      ->GetPlatformProperties()
      .supports_global_screen_coordinates;
#endif
}

bool PlatformClipsChildrenToViewport() {
#if BUILDFLAG(IS_LINUX)
  return true;
#else
  return false;
#endif
}

}  // namespace constrained_window