// Copyright 2025 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/glic/widget/glic_floating_ui.h"

#include "base/feature_list.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/weak_ptr.h"
#include "base/notimplemented.h"
#include "base/time/time.h"
#include "chrome/browser/glic/common/panel_focus_dependent_hotkey_manager.h"
#include "chrome/browser/glic/common/panel_visibility_dependent_hotkey_manager.h"
#include "chrome/browser/glic/glic_profile_manager.h"
#include "chrome/browser/glic/public/features.h"
#include "chrome/browser/glic/service/glic_instance_helper.h"
#include "chrome/browser/glic/service/metrics/glic_instance_metrics.h"
#include "chrome/browser/glic/widget/glic_view.h"
#include "chrome/browser/glic/widget/glic_widget.h"
#include "chrome/browser/glic/widget/glic_window_animator.h"
#include "chrome/browser/picture_in_picture/picture_in_picture_occlusion_tracker.h"
#include "chrome/browser/picture_in_picture/picture_in_picture_window_manager.h"
#include "chrome/browser/profiles/keep_alive/profile_keep_alive_types.h"
#include "chrome/browser/profiles/keep_alive/scoped_profile_keep_alive.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/generated_resources.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "ui/base/base_window.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/views/widget/widget_delegate.h"

#if BUILDFLAG(IS_WIN)
#include "ui/display/win/screen_win.h"
#include "ui/views/win/hwnd_util.h"
#endif  // BUILDFLAG(IS_WIN)

#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/glic/selection/selection_overlay_controller.h"
#endif

namespace glic {

namespace {
BASE_FEATURE(kGlicFloatingUiReattachment, base::FEATURE_ENABLED_BY_DEFAULT);
}  // namespace

// static
gfx::Size GlicFloatingUi::GetDefaultSize() {
  return {features::kGlicMultiInstanceFloatyWidth.Get(),
          features::kGlicMultiInstanceFloatyHeight.Get()};
}

gfx::Size GlicFloatingUi::GetCompositeViewDefaultSize() {
  return {features::kGlicCompositeViewWidth.Get(),
          features::kGlicCompositeViewHeight.Get()};
}
// end static

GlicFloatingUi::GlicFloatingUi(Profile* profile,
                               gfx::Rect initial_bounds,
                               tabs::TabHandle source_tab,
                               GlicUiEmbedder::Delegate& delegate,
                               GlicInstanceMetrics& instance_metrics)
    : profile_(profile),
      delegate_(delegate),
      instance_metrics_(instance_metrics),
      source_tab_(source_tab),
      profile_keep_alive_(profile, ProfileKeepAliveOrigin::kGlicView) {
  if (!base::FeatureList::IsEnabled(features::kGlicOrphanedReattachment)) {
    if (auto* helper = GlicInstanceHelper::From(source_tab_.Get())) {
      source_tab_destruction_subscription_ =
          helper->SubscribeToDestruction(base::BindRepeating(
              &GlicFloatingUi::OnSourceTabDestroyed, base::Unretained(this)));
    }
  }
  panel_visibility_dependent_hotkey_manager_ =
      std::make_unique<PanelVisibilityDependentHotkeyManager>(
          profile_, weak_ptr_factory_.GetWeakPtr());
  panel_focus_dependent_hotkey_manager_ =
      std::make_unique<PanelFocusDependentHotkeyManager>(
          weak_ptr_factory_.GetWeakPtr());
  CreateAndSetupWidget(initial_bounds);
  panel_state_.kind = mojom::PanelStateKind::kDetached;
  PictureInPictureOcclusionTracker* tracker =
      PictureInPictureWindowManager::GetInstance()->GetOcclusionTracker();
  tracker->OnPictureInPictureWidgetOpened(glic_widget_.get());
  browser_attach_observation_ = ObserveBrowserForAttachment(profile_, this);
}

GlicFloatingUi::~GlicFloatingUi() {
  if (IsShowing()) {
    modal_dialog_host_observers_.Notify(
        &web_modal::ModalDialogHostObserver::OnHostDestroying);
  }

  // Only clear the current detached glic if this is being torn
  // down before the profile manager is torn down.
  if (auto* profile_manager = GlicProfileManager::GetInstance()) {
    profile_manager->SetCurrentDetachedGlic(nullptr);
  }

  ClearWebContentsDelegate();
  PictureInPictureOcclusionTracker* tracker =
      PictureInPictureWindowManager::GetInstance()->GetOcclusionTracker();
  tracker->RemovePictureInPictureWidget(glic_widget_.get());
  if (auto* glic_view = GetGlicView()) {
    glic_view->SetWebContents(nullptr);
  }
}

Host::EmbedderDelegate* GlicFloatingUi::GetHostEmbedderDelegate() {
  return this;
}

mojom::PanelState GlicFloatingUi::GetPanelState() const {
  return panel_state_;
}

gfx::Size GlicFloatingUi::GetPanelSize() {
  if (auto* glic_widget = GetGlicWidget()) {
    return glic_widget->GetSize();
  }
  return gfx::Size();
}

GlicWidget* GlicFloatingUi::GetGlicWidget() const {
  return glic_widget_.get();
}

GlicView* GlicFloatingUi::GetGlicView() const {
  if (auto* glic_widget = GetGlicWidget()) {
    return glic_widget->GetGlicView();
  }
  return nullptr;
}

void GlicFloatingUi::CreateAndSetupWidget(gfx::Rect initial_bounds) {
  auto glic_view = std::make_unique<GlicView>(
      profile_, initial_bounds.size(),
      panel_focus_dependent_hotkey_manager_->GetAcceleratorTargetWeakPtr());

  glic_view->SetZoomChangedCallback(base::BindRepeating(
      [](base::WeakPtr<LocalHotkeyManager::Panel> panel, bool zoom_in) {
        if (!panel) {
          return;
        }
        panel->Zoom(
            zoom_in ? mojom::ZoomAction::kZoomIn : mojom::ZoomAction::kZoomOut,
            ZoomSource::kScroll);
      },
      weak_ptr_factory_.GetWeakPtr()));

  glic_view->SetWebContents(delegate_->host().webui_contents());
  glic_delegate_ =
      GlicWidget::CreateWidgetDelegate(std::move(glic_view), user_resizable_);
  glic_delegate_->SetAccessibleTitle(
      l10n_util::GetStringUTF16(IDS_GLIC_WINDOW_TITLE));
  glic_widget_ = GlicWidget::Create(glic_delegate_.get(), profile_,
                                    initial_bounds, user_resizable_);

#if BUILDFLAG(IS_MAC)
  GetGlicWidget()->SetActivationIndependence(true);
  GetGlicWidget()->SetVisibleOnAllWorkspaces(true);
  GetGlicWidget()->SetCanAppearInExistingFullscreenSpaces(true);
#endif

  glic_window_animator_ = std::make_unique<GlicWindowAnimator>(
      glic_widget_->GetWeakPtr(),
      base::BindRepeating(&GlicFloatingUi::MaybeSetWidgetCanResize,
                          weak_ptr_factory_.GetWeakPtr()));
  glic_widget_observation_.Observe(GetGlicWidget());
}

void GlicFloatingUi::Resize(const gfx::Size& size,
                            base::TimeDelta duration,
                            base::OnceClosure callback) {
  if (!user_resizing_ && glic_window_animator_ && IsShowing()) {
    glic_window_animator_->AnimateSize(
        GlicWidget::ClampSize(size, GetGlicWidget()), duration,
        std::move(callback));
  } else {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE, std::move(callback));
  }
}

void GlicFloatingUi::FocusIfOpen() {
  if (!IsShowing() || HasFocus()) {
    return;
  }
  GetGlicWidget()->Activate();
  GetGlicView()->GetWebContents()->Focus();
}

bool GlicFloatingUi::HasFocus() {
  return IsShowing() && GetGlicWidget()->IsActive();
}

bool GlicFloatingUi::ActivateBrowser() {
  if (auto* const last_active_bwi =
          GetLastActiveBrowserWindowInterfaceWithAnyProfile()) {
    last_active_bwi->GetWindow()->Activate();
    return true;
  }
  return false;
}

void GlicFloatingUi::Zoom(mojom::ZoomAction zoom_action, ZoomSource source) {
  delegate_->host().Zoom(zoom_action, source);
}

void GlicFloatingUi::ShowTitleBarContextMenuAt(gfx::Point event_loc) {
#if BUILDFLAG(IS_WIN)
  views::View::ConvertPointToScreen(GetGlicView(), &event_loc);
  event_loc = display::win::GetScreenWin()->DIPToScreenPoint(event_loc);
  views::ShowSystemMenuAtScreenPixelLocation(views::HWNDForView(GetGlicView()),
                                             event_loc);
#endif  // BUILDFLAG(IS_WIN)
}

#if !BUILDFLAG(IS_ANDROID)
bool GlicFloatingUi::HasSelectionOverlay() {
  tabs::TabInterface* focused_tab =
      delegate_->host().GetSharingManagerInternal().GetFocusedTabData().focus();
  if (!focused_tab || !focused_tab->IsActivated()) {
    return false;
  }
  auto* selection_overlay_controller =
      SelectionOverlayController::FromTabWebContents(
          focused_tab->GetContents());
  return selection_overlay_controller->state() ==
         SelectionOverlayController::State::kOverlay;
}

void GlicFloatingUi::CloseSelectionOverlay() {
  tabs::TabInterface* focused_tab =
      delegate_->host().GetSharingManagerInternal().GetFocusedTabData().focus();
  if (!focused_tab || !focused_tab->IsActivated()) {
    return;
  }
  auto* selection_overlay_controller =
      SelectionOverlayController::FromTabWebContents(
          focused_tab->GetContents());
  if (!selection_overlay_controller) {
    return;
  }
  selection_overlay_controller->Close();
}
#endif

void GlicFloatingUi::EnableDragResize(bool enabled) {
  user_resizable_ = enabled;

  if (!GetGlicWidget() || !GetGlicWidget()->widget_delegate()) {
    return;
  }

  MaybeSetWidgetCanResize();
  if (auto* glic_view = GetGlicView()) {
    glic_view->UpdateBackgroundColor();
  }
  glic_window_animator_->MaybeAnimateToTargetSize();
}

void GlicFloatingUi::MaybeSetWidgetCanResize() {
  // During teardown, the widget's delegate is null. Just return early.
  if (!GetGlicWidget()->widget_delegate()) {
    return;
  }

  if (GetGlicWidget()->widget_delegate()->CanResize() == user_resizable_ ||
      glic_window_animator_->IsAnimating()) {
    // If the resize state is already correct or the widget is animating do not
    // update the resize state.
    return;
  }

#if BUILDFLAG(IS_WIN)
  // On Windows when resize is enabled there is an invisible border added
  // around the client area. We need to make the widget larger or smaller to
  // keep the visible client area the same size.
  gfx::Rect previous_client_bounds =
      GetGlicWidget()->GetClientAreaBoundsInScreen();
#endif  // BUILDFLAG(IS_WIN)

  // Update resize state on widget delegate.
  GetGlicWidget()->widget_delegate()->SetCanResize(user_resizable_);

#if BUILDFLAG(IS_WIN)
  if (user_resizable_) {
    // Resizable so the widget area is larger than the client area.
    gfx::Rect new_widget_bounds =
        GetGlicWidget()->VisibleToWidgetBounds(previous_client_bounds);
    GetGlicWidget()->SetBoundsConstrained(new_widget_bounds);
  } else {
    // Not resizable so the client and widget areas are the same.
    GetGlicWidget()->SetBoundsConstrained(previous_client_bounds);
  }
#endif  // BUILDFLAG(IS_WIN)
}

void GlicFloatingUi::OnSourceTabDestroyed(tabs::TabInterface* tab) {
  FloatingPanelCanAttachChanged(false);
}

void GlicFloatingUi::FloatingPanelCanAttachChanged(bool can_attach) {
  if (!base::FeatureList::IsEnabled(kGlicFloatingUiReattachment)) {
    return;
  }
  delegate_->host().FloatingPanelCanAttachChanged(can_attach);
}

void GlicFloatingUi::CanAttachToBrowserChanged(bool can_attach) {
  FloatingPanelCanAttachChanged(can_attach && source_tab_.Get() != nullptr);
}

void GlicFloatingUi::ConfigureWebContentsModalDialogs() {
  // Add capability to show web modal dialogs (e.g. Data Controls Dialogs for
  // enterprise users) via constrained_window APIs.
  web_modal::WebContentsModalDialogManager::CreateForWebContents(
      delegate_->host().webui_contents());
  web_modal::WebContentsModalDialogManager::FromWebContents(
      delegate_->host().webui_contents())
      ->SetDelegate(this);
}

void GlicFloatingUi::Attach() {
  if (!base::FeatureList::IsEnabled(kGlicFloatingUiReattachment)) {
    return;
  }

  // NOTE: `this` will be destroyed after this call.
  delegate_->Attach(source_tab_);
}

void GlicFloatingUi::Detach() {
  // Floaty UI is already detached.
  LOG(WARNING) << "GlicFloatingUi: Detach() called while already detached.";
}

void GlicFloatingUi::SetMinimumWidgetSize(const gfx::Size& size) {
  GetGlicWidget()->SetMinimumSize(size);
}

bool GlicFloatingUi::IsShowing() const {
  return glic_widget_ != nullptr;
}

bool GlicFloatingUi::IsShowingOrBackgrounded() const {
  return IsShowing();
}

void GlicFloatingUi::Show(const ShowOptions& options) {
  FloatingPanelCanAttachChanged(
      browser_attach_observation_->CanAttachToBrowser() &&
      source_tab_.Get() != nullptr);
  instance_metrics_->OnShowInFloaty(options);
  GlicProfileManager::GetInstance()->SetCurrentDetachedGlic(profile_);
  GetGlicWidget()->Show();
  GetGlicView()->UpdateBackgroundColor();
  panel_visibility_dependent_hotkey_manager_->InitializeAccelerators();
  panel_focus_dependent_hotkey_manager_->InitializeAccelerators();
  ConfigureWebContentsModalDialogs();
}

void GlicFloatingUi::Close(const CloseOptions& options) {
  instance_metrics_->OnFloatyClosed();
  if (IsShowing()) {
    modal_dialog_host_observers_.Notify(
        &web_modal::ModalDialogHostObserver::OnHostDestroying);
  }
  ClearWebContentsDelegate();
  CloseSelectionOverlay();
  if (screenshot_capturer_) {
    screenshot_capturer_->CloseScreenPicker();
  }
  FloatingPanelCanAttachChanged(false);
  glic_window_animator_.reset();
  glic_widget_observation_.Reset();
  glic_widget_.reset();
  glic_delegate_.reset();
  user_resizable_ = false;
  // NOTE: `this` will be destroyed after this call.
  delegate_->DidCloseFor(FloatingEmbedderKey{},
                         EmbedderCloseReason::kExplicitlyClosed);
}

void GlicFloatingUi::ClearWebContentsDelegate() {
  if (auto* web_contents = delegate_->host().webui_contents()) {
    auto* dialog_manager =
        web_modal::WebContentsModalDialogManager::FromWebContents(web_contents);
    if (dialog_manager->delegate() == this) {
      dialog_manager->SetDelegate(nullptr);
    }
  }
}

void GlicFloatingUi::OnReload() {
  if (auto* glic_view = GetGlicView()) {
    glic_view->SetWebContents(delegate_->host().webui_contents());
    ConfigureWebContentsModalDialogs();
  }
}

void GlicFloatingUi::MaybeNotifyActivationChanged(bool window_active) {
  bool active = window_active || (delegate_->host().microphone_status() ==
                                  mojom::MicrophoneStatus::kListening);
  delegate_->OnEmbedderWindowActivationChanged(active);
}

void GlicFloatingUi::OnMicrophoneStatusChanged(mojom::MicrophoneStatus status) {
  MaybeNotifyActivationChanged(HasFocus());
}

void GlicFloatingUi::Focus() {
  if (!IsShowing()) {
    return;
  }
  GetGlicWidget()->Activate();
  if (auto* web_contents = delegate_->host().webui_contents()) {
    web_contents->Focus();
  }
}

void GlicFloatingUi::OnWidgetActivationChanged(views::Widget* widget,
                                               bool active) {
  MaybeNotifyActivationChanged(active);
}

void GlicFloatingUi::OnWidgetDestroyed(views::Widget* widget) {
  // This is used to handle the case where the native window is closed
  // directly (e.g., Windows context menu close on the title bar).
  // Conceptually this should synchronously call Close(), but the Widget
  // implementation currently does not support this.
  if (GetGlicWidget() == widget) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(&GlicFloatingUi::Close, weak_ptr_factory_.GetWeakPtr(),
                       CloseOptions{}));
  }
}

void GlicFloatingUi::OnWidgetBoundsChanged(views::Widget* widget,
                                           const gfx::Rect& new_bounds) {
  modal_dialog_host_observers_.Notify(
      &web_modal::ModalDialogHostObserver::OnPositionRequiresUpdate);
}

void GlicFloatingUi::OnWidgetUserResizeStarted(views::Widget* widget) {
  user_resizing_ = true;
  instance_metrics_->OnUserResizeStarted(GetPanelSize());
  delegate_->host().ManualResizeChanged(true);
}

void GlicFloatingUi::OnWidgetUserResizeEnded(views::Widget* widget) {
  instance_metrics_->OnUserResizeEnded(GetPanelSize());
  delegate_->host().ManualResizeChanged(false);

  glic_window_animator_->ResetLastTargetSize();
  user_resizing_ = false;
}

// web_modal::WebContentsModalDialogManagerDelegate
// web_modal::WebContentsModalDialogHost
web_modal::WebContentsModalDialogHost*
GlicFloatingUi::GetWebContentsModalDialogHost(
    content::WebContents* web_contents) {
  return this;
}

gfx::Size GlicFloatingUi::GetMaximumDialogSize() {
  // Print preview might be the widest model dialog we support for now, use its
  // min size if FLoaty is smaller than that.
  gfx::Size floaty_size = GetGlicWidget()->GetClientAreaBoundsInScreen().size();
  gfx::Size default_size = GetCompositeViewDefaultSize();
  if (floaty_size.width() >= default_size.width()) {
    return floaty_size;
  }
  return default_size;
}

gfx::NativeView GlicFloatingUi::GetHostView() const {
  return GetGlicWidget()->GetNativeView();
}

gfx::Point GlicFloatingUi::GetDialogPosition(const gfx::Size& dialog_size) {
  gfx::Rect client_area_bounds = GetGlicWidget()->GetClientAreaBoundsInScreen();
  return gfx::Point((client_area_bounds.width() - dialog_size.width()) / 2, 0);
}

bool GlicFloatingUi::ShouldConstrainDialogBoundsByHost() {
  // Allows web modal dialogs to extend beyond the boundary of glic window.
  // These web modals are usually larger than the glic window.
  return false;
}

void GlicFloatingUi::AddObserver(web_modal::ModalDialogHostObserver* observer) {
  modal_dialog_host_observers_.AddObserver(observer);
}

void GlicFloatingUi::RemoveObserver(
    web_modal::ModalDialogHostObserver* observer) {
  modal_dialog_host_observers_.RemoveObserver(observer);
}

std::unique_ptr<GlicUiEmbedder> GlicFloatingUi::CreateInactiveEmbedder() const {
  return nullptr;
}

#if !BUILDFLAG(IS_ANDROID)
base::WeakPtr<views::View> GlicFloatingUi::GetView() {
  if (auto* glic_view = GetGlicView()) {
    return glic_view->GetWeakPtr();
  }
  return nullptr;
}
#endif

void GlicFloatingUi::SwitchConversation(
    glic::mojom::ConversationInfoPtr info,
    mojom::WebClientHandler::SwitchConversationCallback callback) {
  // NOTE: `this` may be destroyed after this call.
  delegate_->SwitchConversation(
      ShowOptions::ForFloating(GetGlicWidget()->GetWindowBoundsInScreen()),
      std::move(info), std::move(callback));
}

void GlicFloatingUi::CaptureScreenshot(
    glic::mojom::WebClientHandler::CaptureScreenshotCallback callback) {
  if (!screenshot_capturer_) {
    screenshot_capturer_ = GlicScreenshotCapturer::Create();
  }
  screenshot_capturer_->CaptureScreenshot(GetGlicWidget()->GetNativeWindow(),
                                          std::move(callback));
}

void GlicFloatingUi::ClosePanel() {
  Close({});
}

std::string GlicFloatingUi::DescribeForTesting() {
  return "FloatingUi";
}

}  // namespace glic
