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

#include <memory>
#include <optional>

#include "ash/multi_user/multi_user_window_manager.h"
#include "ash/shell.h"
#include "ash/wm/window_state.h"
#include "base/check.h"
#include "base/check_op.h"
#include "base/metrics/user_metrics.h"
#include "build/build_config.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/ash/session/session_util.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
#include "chrome/browser/ui/immersive/immersive_mode_controller.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/browser_widget.h"
#include "chrome/browser/ui/views/frame/tab_strip_region_view.h"
#include "chrome/browser/ui/views/frame/top_container_view.h"
#include "chrome/browser/ui/views/profiles/profile_indicator_icon.h"
#include "chrome/browser/ui/views/side_panel/side_panel.h"
#include "chrome/browser/ui/views/tab_icon_view.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/browser/ui/web_applications/app_browser_controller.h"
#include "chrome/browser/ui/window_feature_controller/window_feature_controller.h"
#include "chrome/browser/ui/window_metadata/window_metadata_controller.h"
#include "chromeos/ash/experiences/system_web_apps/types/system_web_app_delegate.h"
#include "chromeos/components/kiosk/kiosk_utils.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/ui/base/chromeos_ui_constants.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/base/window_state_type.h"
#include "chromeos/ui/frame/caption_buttons/frame_caption_button_container_view.h"
#include "chromeos/ui/frame/default_frame_header.h"
#include "chromeos/ui/frame/default_highlight_border_overlay_delegate.h"
#include "chromeos/ui/frame/frame_utils.h"
#include "chromeos/ui/wm/window_util.h"
#include "components/services/app_service/public/cpp/app_update.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/env.h"
#include "ui/base/hit_test.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/base/mojom/window_show_state.mojom.h"
#include "ui/base/ui_base_types.h"
#include "ui/chromeos/styles/cros_styles.h"
#include "ui/display/screen.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/webview/webview.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/metadata/view_factory.h"
#include "ui/views/rect_based_targeting_utils.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/window/caption_button_layout_constants.h"

DEFINE_UI_CLASS_PROPERTY_TYPE(BrowserFrameViewChromeOS*)

namespace {

// The indicator for teleported windows has 8 DIPs before and below it.
constexpr int kProfileIndicatorPadding = 8;

DEFINE_UI_CLASS_PROPERTY_KEY(BrowserFrameViewChromeOS*,
                             kBrowserFrameViewChromeOSKey,
                             nullptr)

// Returns true if the header should be painted so that it looks the same as
// the header used for packaged apps.
bool UsePackagedAppHeaderStyle(const BrowserWindowInterface* browser) {
  if (browser->GetType() == BrowserWindowInterface::Type::TYPE_NORMAL ||
      (browser->GetType() == BrowserWindowInterface::Type::TYPE_POPUP &&
       !WindowFeatureController::From(browser)->IsTrustedSource())) {
    return false;
  }

  return !WindowFeatureController::From(browser)->SupportsWindowFeature(
      WindowFeatureController::WindowFeature::kFeatureTabStrip);
}

// Whether or not the window's title should show the avatar. Practically,
// returns true when the owner of the window is different from the owner of
// the desktop.
bool ShouldShowAvatar(aura::Window* window) {
  auto* multi_user_window_manager =
      ash::Shell::Get()->multi_user_window_manager();
  return !multi_user_window_manager->IsWindowOnDesktopOfUser(
      window, multi_user_window_manager->GetWindowOwner(window));
}

}  // namespace

class BrowserFrameViewChromeOS::ProfileChangeObserver
    : public ProfileAttributesStorage::Observer {
 public:
  explicit ProfileChangeObserver(BrowserFrameViewChromeOS& frame)
      : frame_(frame) {
    if (g_browser_process->profile_manager()) {
      profile_observation_.Observe(
          &g_browser_process->profile_manager()->GetProfileAttributesStorage());
    } else {
      CHECK_IS_TEST();
    }
  }

  ~ProfileChangeObserver() override = default;

  // ProfileAttributesStorage::Observer:
  void OnProfileAdded(const base::FilePath& profile_path) override {
    frame_->UpdateProfileIcons();
  }
  void OnProfileWasRemoved(const base::FilePath& profile_path,
                           const std::u16string& profile_name) override {
    frame_->UpdateProfileIcons();
  }
  void OnProfileAvatarChanged(const base::FilePath& profile_path) override {
    frame_->UpdateProfileIcons();
  }
  void OnProfileHighResAvatarLoaded(
      const base::FilePath& profile_path) override {
    frame_->UpdateProfileIcons();
  }

 private:
  raw_ref<BrowserFrameViewChromeOS> frame_;
  base::ScopedObservation<ProfileAttributesStorage,
                          ProfileAttributesStorage::Observer>
      profile_observation_{this};
};

BrowserFrameViewChromeOS::BrowserFrameViewChromeOS(BrowserWidget* widget,
                                                   BrowserView* browser_view)
    : BrowserFrameView(widget, browser_view) {
  chromeos::wm::InstallResizeHandleWindowTargeterForWindow(
      widget->GetNativeWindow());

  aura::Window* frame_window = widget->GetNativeWindow();
  frame_window->SetProperty(kBrowserFrameViewChromeOSKey, this);

  GetViewAccessibility().SetRole(ax::mojom::Role::kTitleBar);
}

BrowserFrameViewChromeOS::~BrowserFrameViewChromeOS() {
  if (auto* immersive_controller = GetImmersiveModeController()) {
    immersive_controller->RemoveObserver(this);
  }

  if (profile_indicator_icon_) {
    RemoveChildViewT(std::exchange(profile_indicator_icon_, nullptr));
  }
}

BrowserFrameViewChromeOS* BrowserFrameViewChromeOS::Get(aura::Window* window) {
  return window->GetProperty(kBrowserFrameViewChromeOSKey);
}

void BrowserFrameViewChromeOS::Init() {
  BrowserWindowInterface* browser = GetBrowserView()->browser();

  auto* const app_controller = web_app::AppBrowserController::From(browser);
  const bool is_close_button_enabled =
      !(app_controller && app_controller->IsPreventCloseEnabled());

  caption_button_container_ =
      AddChildView(std::make_unique<chromeos::FrameCaptionButtonContainerView>(
          browser_widget(), is_close_button_enabled));

  // Initializing the TabIconView is expensive, so only do it if we need to.
  if (GetBrowserView()->ShouldShowWindowIcon()) {
    AddChildView(views::Builder<TabIconView>()
                     .CopyAddressTo(&window_icon_)
                     .SetModel(this)
                     .Build());
  }

  UpdateProfileIcons();

  window_observation_.Observe(GetFrameWindow());

  if (apps::AppServiceProxyFactory::IsAppServiceAvailableForProfile(
          browser->GetProfile())) {
    app_registry_cache_observation_.Observe(
        &apps::AppServiceProxyFactory::GetForProfile(browser->GetProfile())
             ->AppRegistryCache());
  }

  // To preserve privacy, tag incognito windows so that they won't be included
  // in screenshot sent to assistant server.
  if (browser->GetProfile()->IsOffTheRecord()) {
    browser_widget()->GetNativeWindow()->SetProperty(
        chromeos::kBlockedForAssistantSnapshotKey, true);
  }

  display_observer_.emplace(this);
  frame_header_ = CreateFrameHeader();

  // BrowserFrameViewChromeOS should not paint titles for Web Apps given that
  // client view (BrowserView) paints/manages the window title for Web Apps.
  // (See `BrowserView::web_app_window_title_`)
  if (GetBrowserView()->GetIsWebAppType()) {
    frame_header_->SetPaintTitleBar(false);
  }

  if (AppIsPwaWithUnframedDisplayMode()) {
    UpdateUnframedModeEnabled();
  }

  GetImmersiveModeController()->AddObserver(this);
}

BrowserLayoutParams BrowserFrameViewChromeOS::GetBrowserLayoutParams() const {
  BrowserLayoutParams params;
  params.visual_client_area = GetLocalBounds();
  if (profile_indicator_icon_) {
    params.leading_exclusion.content =
        gfx::SizeF(profile_indicator_icon_->bounds().right(),
                   profile_indicator_icon_->bounds().bottom());
    params.leading_exclusion.horizontal_padding = kProfileIndicatorPadding;
    params.leading_exclusion.vertical_padding =
        profile_indicator_icon_->bounds().y();
  }
  if (GetShowCaptionButtonsWhenNotInOverview()) {
    const auto caption_bounds = caption_button_container_->bounds();
    // Cut the caption buttons down to the maximum height of the tabstrip if
    // present.
    const auto elements = GetClientFrameElementInfo();
    const int height = elements.tabstrip_preferred_height
                           ? elements.tabstrip_preferred_height
                           : caption_bounds.height();
    params.trailing_exclusion.content =
        gfx::SizeF(width() - caption_bounds.x(), height);
    MaybeAddAppIconToLayoutParams(params);
  }
  return params;
}

bool BrowserFrameViewChromeOS::ShouldShowWebAppFrameToolbar() const {
  if (!GetShowCaptionButtons()) {
    return false;
  }

  return true;
}

int BrowserFrameViewChromeOS::GetTopInset(bool restored) const {
  // TODO(estade): why do callsites in this class hardcode false for |restored|?

  if (!GetShouldPaint()) {
    // When immersive fullscreen unrevealed, tabstrip is offscreen with normal
    // tabstrip bounds, the top inset should reach this topmost edge.
    const auto* const immersive_controller = GetImmersiveModeController();
    if (immersive_controller->IsEnabled() &&
        !immersive_controller->IsRevealed()) {
      return (-1) * GetClientFrameElementInfo().top_area_height();
    }

    // The header isn't painted for restored popup/app windows in overview mode,
    // but the inset is still calculated below, so the overview code can align
    // the window content with a fake header.
    if (!GetOverviewMode() || browser_widget()->IsFullscreen() ||
        GetBrowserView()->GetTabStripVisible()) {
      return 0;
    }
  }

  if (GetBrowserView()->GetTabStripVisible()) {
    return 0;
  }

  BrowserWindowInterface* browser = GetBrowserView()->browser();

  int header_height = frame_header_->GetHeaderHeight();
  const gfx::Size toolbar_size =
      GetBrowserView()->GetWebAppFrameToolbarPreferredSize();
  if (!toolbar_size.IsEmpty()) {
    header_height = std::max(header_height, toolbar_size.height());
  }

  return UsePackagedAppHeaderStyle(browser)
             ? header_height
             : caption_button_container_->bounds().bottom();
}

int BrowserFrameViewChromeOS::GetTargetHeaderHeight() const {
  // For normal tabbed windows, the header height is determined by the tabstrip.
  const int tabstrip_preferred_height =
      GetClientFrameElementInfo().tabstrip_preferred_height;
  if (tabstrip_preferred_height > 0) {
    return tabstrip_preferred_height;
  }

  // For non-tabbed windows (e.g. popups, devtools, web apps), start with the
  // default caption button layout size.
  int target_height =
      views::GetCaptionButtonLayoutSize(frame_header_->GetButtonLayoutSize())
          .height();

  // Web apps / PWAs may have a frame toolbar that requires additional height.
  const gfx::Size toolbar_size =
      GetBrowserView()->GetWebAppFrameToolbarPreferredSize();
  if (!toolbar_size.IsEmpty()) {
    target_height = std::max(target_height, toolbar_size.height());
  }

  return target_height;
}

void BrowserFrameViewChromeOS::UpdateThrobber(bool running) {
  if (window_icon_) {
    window_icon_->Update();
  }
}

SkColor BrowserFrameViewChromeOS::GetCaptionColor(
    BrowserFrameActiveState active_state) const {
  // Web apps apply a theme color if specified by the extension/manifest.
  std::optional<SkColor> frame_theme_color =
      web_app::AppBrowserController::From(GetBrowserView()->browser())
          ->GetThemeColor();
  const SkColor frame_color =
      frame_theme_color.value_or(GetFrameColor(active_state));
  const SkColor active_caption_color =
      views::FrameCaptionButton::GetButtonColor(frame_color);

  if (ShouldPaintAsActiveForState(active_state)) {
    return active_caption_color;
  }

  const float inactive_alpha_ratio =
      views::FrameCaptionButton::GetInactiveButtonColorAlphaRatio();
  return SkColorSetA(active_caption_color,
                     inactive_alpha_ratio * SK_AlphaOPAQUE);
}

SkColor BrowserFrameViewChromeOS::GetFrameColor(
    BrowserFrameActiveState active_state) const {
  if (!UsePackagedAppHeaderStyle(GetBrowserView()->browser())) {
    return BrowserFrameView::GetFrameColor(active_state);
  }

  std::optional<SkColor> color;
  if (GetBrowserView()->GetIsWebAppType()) {
    color = web_app::AppBrowserController::From(GetBrowserView()->browser())
                ->GetThemeColor();
  }

  SkColor fallback_color = chromeos::kDefaultFrameColor;

  if (GetWidget()) {
    // TODO(skau): Migrate to ColorProvider.
    fallback_color = cros_styles::ResolveColor(
        cros_styles::ColorName::kBgColor,
        GetNativeTheme()->preferred_color_scheme() ==
            ui::NativeTheme::PreferredColorScheme::kDark);
  }

  return color.value_or(fallback_color);
}

void BrowserFrameViewChromeOS::UpdateMinimumSize() {
  gfx::Size current_min_size = GetMinimumSize();
  if (last_minimum_size_ == current_min_size) {
    return;
  }

  last_minimum_size_ = current_min_size;
  GetWidget()->OnSizeConstraintsChanged();
}

gfx::Rect BrowserFrameViewChromeOS::GetBoundsForClientView() const {
  // The ClientView must be flush with the top edge of the widget so that the
  // web contents can take up the entire screen in immersive fullscreen (with
  // or without the top-of-window views revealed). When in immersive fullscreen
  // and the top-of-window views are revealed, the TopContainerView paints the
  // window header by redirecting paints from its background to
  // BrowserFrameViewChromeOS.
  return bounds();
}

gfx::Rect BrowserFrameViewChromeOS::GetWindowBoundsForClientBounds(
    const gfx::Rect& client_bounds) const {
  const int top_inset = GetTopInset(false);
  return gfx::Rect(client_bounds.x(),
                   std::max(0, client_bounds.y() - top_inset),
                   client_bounds.width(), client_bounds.height() + top_inset);
}

int BrowserFrameViewChromeOS::NonClientHitTest(const gfx::Point& point) {
  int hit_test = chromeos::FrameBorderNonClientHitTest(
      this, point, non_client_hit_test_callback_);

  // When the window is restored (and not in tablet split-view mode) we want a
  // large click target above the tabs to drag the window, so redirect clicks in
  // the tab's shadow to caption.
  if (hit_test == HTCLIENT &&
      chromeos::ShouldShowResizeBorder(GetWidget()->GetNativeWindow())) {
    // TODO(crbug.com/40768579): Tab Strip hit calculation and bounds logic
    // should reside in the TabStrip class.
    gfx::Point client_point(point);
    View::ConvertPointToTarget(this, browser_widget()->client_view(),
                               &client_point);
    gfx::Rect tabstrip_shadow_bounds(
        GetBrowserView()->tab_strip_view()->GetTabStripView()->bounds());
    constexpr int kTabShadowHeight = 4;
    tabstrip_shadow_bounds.set_height(kTabShadowHeight);
    if (tabstrip_shadow_bounds.Contains(client_point)) {
      return HTCAPTION;
    }
  }

  return hit_test;
}

void BrowserFrameViewChromeOS::ResetWindowControls() {
  BrowserFrameView::ResetWindowControls();
  caption_button_container_->SetVisible(GetShowCaptionButtons());
  caption_button_container_->ResetWindowControls();
}

void BrowserFrameViewChromeOS::WindowControlsOverlayEnabledChanged() {
  bool enabled = GetBrowserView()->IsWindowControlsOverlayEnabled();
  caption_button_container_->OnWindowControlsOverlayEnabledChanged(
      enabled, GetFrameHeaderColor(GetBrowserView()->IsActive()));
}

void BrowserFrameViewChromeOS::UpdateWindowIcon() {
  if (window_icon_) {
    window_icon_->SchedulePaint();
  }
}

void BrowserFrameViewChromeOS::UpdateWindowTitle() {
  if (!browser_widget()->IsFullscreen()) {
    frame_header_->SchedulePaintForTitle();
  }

  browser_widget()->GetNativeWindow()->SetProperty(
      chromeos::kWindowOverviewTitleKey,
      WindowMetadataController::From(GetBrowserView()->browser())
          ->GetWindowTitleForCurrentTab(
              /*include_app_name=*/false));
}

void BrowserFrameViewChromeOS::SizeConstraintsChanged() {}

void BrowserFrameViewChromeOS::OnPaint(gfx::Canvas* canvas) {
  if (!GetShouldPaint()) {
    return;
  }

  frame_header_->PaintHeader(canvas);
}

void BrowserFrameViewChromeOS::UpdateUnframedModeEnabled() {
  caption_button_container_->UpdateUnframedModeEnabled(
      GetBrowserView()->IsUnframedModeEnabled());
}

bool BrowserFrameViewChromeOS::AppIsPwaWithUnframedDisplayMode() const {
  return GetBrowserView()->GetIsWebAppType() &&
         GetBrowserView()->AppUsesUnframedMode();
}

void BrowserFrameViewChromeOS::Layout(PassKey) {
  // Set the painted height early so that LayoutHeader() can use it to size
  // the caption buttons correctly.
  frame_header_->SetHeaderHeightForPainting(GetTargetHeaderHeight());

  frame_header_->LayoutHeader();

  if (profile_indicator_icon_) {
    LayoutProfileIndicator();
  }

  if (AppIsPwaWithUnframedDisplayMode()) {
    UpdateUnframedModeEnabled();
  }

  LayoutSuperclass<BrowserFrameView>(this);
  UpdateTopViewInset();

  // The top right corner must be occupied by a caption button for easy mouse
  // access. This check is agnostic to RTL layout.
  DCHECK_EQ(caption_button_container_->y(), 0);
  DCHECK_EQ(caption_button_container_->bounds().right(), width());
}

gfx::Size BrowserFrameViewChromeOS::GetMinimumSize() const {
  // System web apps (e.g. Settings) may have a fixed minimum size.
  const auto* const swa_delegate =
      web_app::GetSystemWebAppDelegate(GetBrowserView()->browser());
  if (swa_delegate) {
    gfx::Size minimum_size = swa_delegate->GetMinimumWindowSize();
    if (!minimum_size.IsEmpty()) {
      return minimum_size;
    }
  }

  if (GetBrowserView()->IsUnframedModeEnabled()) {
    // The minimum size of windows in unframed mode is the size of
    // `highlight_border_overlay_`.
    return highlight_border_overlay_->CalculateImageSourceSize();
  }

  gfx::Size min_client_view_size(
      browser_widget()->client_view()->GetMinimumSize());
  const int min_frame_width = frame_header_->GetMinimumHeaderWidth();
  int min_width = std::max(min_frame_width, min_client_view_size.width());
  if (GetBrowserView()->GetTabStripVisible()) {
    // Ensure that the minimum width is enough to hold a minimum width tab strip
    // at its usual insets.
    const int min_tabstrip_width =
        GetBrowserView()->tab_strip_view()->GetMinimumSize().width();
    min_width =
        std::max(min_width, min_tabstrip_width + GetTabStripLeftInset() +
                                GetTabStripRightInset());
  }

  int min_height = min_client_view_size.height();
  if (GetBrowserView()->IsWindowControlsOverlayEnabled()) {
    // Ensure that the minimum height is at least the height of the caption
    // button container, which contains the WCO toggle and other windowing
    // controls.
    min_height = min_height + caption_button_container_->size().height();
  }

  // Include bottom rounded corners region. See b:294588040.
  const gfx::RoundedCornersF window_radii = GetWindowRoundedCorners();
  CHECK_EQ(window_radii.lower_left(), window_radii.lower_right());

  min_height = min_height + window_radii.lower_left();

  return gfx::Size(min_width, min_height);
}

void BrowserFrameViewChromeOS::OnThemeChanged() {
  OnUpdateFrameColor();
  caption_button_container_->OnWindowControlsOverlayEnabledChanged(
      GetBrowserView()->IsWindowControlsOverlayEnabled(),
      GetFrameHeaderColor(GetBrowserView()->IsActive()));
  BrowserFrameView::OnThemeChanged();
}

void BrowserFrameViewChromeOS::ChildPreferredSizeChanged(views::View* child) {
  if (GetBrowserView()->initialized()) {
    InvalidateLayout();
    browser_widget()->GetRootView()->DeprecatedLayoutImmediately();
  }
}

views::View::Views BrowserFrameViewChromeOS::GetChildrenInZOrder() {
  return frame_header_->GetAdjustedChildrenInZOrder(this);
}

SkColor BrowserFrameViewChromeOS::GetTitleColor() {
  return GetColorProvider()->GetColor(kColorCaptionForeground);
}

SkColor BrowserFrameViewChromeOS::GetFrameHeaderColor(bool active) {
  return GetFrameColor(active ? BrowserFrameActiveState::kActive
                              : BrowserFrameActiveState::kInactive);
}

gfx::ImageSkia BrowserFrameViewChromeOS::GetFrameHeaderImage(bool active) {
  return GetFrameImage(active ? BrowserFrameActiveState::kActive
                              : BrowserFrameActiveState::kInactive);
}

int BrowserFrameViewChromeOS::GetFrameHeaderImageYInset() {
  return GetBrowserView()->GetThemeOffsetFromBrowserView().y();
}

gfx::ImageSkia BrowserFrameViewChromeOS::GetFrameHeaderOverlayImage(
    bool active) {
  return GetFrameOverlayImage(active ? BrowserFrameActiveState::kActive
                                     : BrowserFrameActiveState::kInactive);
}

void BrowserFrameViewChromeOS::OnDisplayTabletStateChanged(
    display::TabletState state) {
  switch (state) {
    case display::TabletState::kInTabletMode:
      OnTabletModeToggled(true);
      return;
    case display::TabletState::kInClamshellMode:
      OnTabletModeToggled(false);
      return;
    case display::TabletState::kEnteringTabletMode:
    case display::TabletState::kExitingTabletMode:
      break;
  }
}

void BrowserFrameViewChromeOS::OnDisplayMetricsChanged(
    const display::Display& display,
    uint32_t changed_metrics) {
  // When the display is rotated, the frame header may have invalid snap icons.
  // For example, rotating from landscape display to portrait display layout
  // should update snap icons from left/right arrows to upward/downward arrows
  // for top and bottom snaps.
  if (changed_metrics & DISPLAY_METRIC_ROTATION) {
    frame_header_->InvalidateLayout();
  }
}

void BrowserFrameViewChromeOS::OnTabletModeToggled(bool enabled) {
  auto* const immersive_mode_controller = GetImmersiveModeController();

  if (!enabled && immersive_mode_controller->IsRevealed()) {
    // Before updating the caption buttons state below (which triggers a
    // relayout), we want to move the caption buttons from the
    // TopContainerView back to this view.
    OnImmersiveRevealEnded();
  }

  const bool should_show_caption_buttons = GetShowCaptionButtons();
  caption_button_container_->SetVisible(should_show_caption_buttons);
  caption_button_container_->UpdateCaptionButtonState(true /*=animate*/);

  const bool was_immersive = immersive_mode_controller->IsEnabled();

  // Set the immersive mode to what it should be because an immersive mode may
  // be used in non fullscreen state in tablet mode.
  immersive_mode_controller->SetEnabled(ShouldEnableImmersiveModeController());

  // Do not relayout if immersive mode hasn't changed because the non client
  // frame area will not change.
  if (was_immersive == immersive_mode_controller->IsEnabled()) {
    return;
  }

  InvalidateLayout();
  // Can be null in tests.
  if (browser_widget()->client_view()) {
    browser_widget()->client_view()->InvalidateLayout();
  }
  if (browser_widget()->GetRootView()) {
    browser_widget()->GetRootView()->DeprecatedLayoutImmediately();
  }
}

bool BrowserFrameViewChromeOS::ShouldTabIconViewAnimate() const {
  // Web apps use their app icon and shouldn't show a throbber.
  if (GetBrowserView()->GetIsWebAppType()) {
    return false;
  }

  // This function is queried during the creation of the window as the
  // TabIconView we host is initialized, so we need to null check the selected
  // WebContents because in this condition there is not yet a selected tab.
  content::WebContents* current_tab = GetBrowserView()->GetActiveWebContents();
  return current_tab && current_tab->ShouldShowLoadingUI();
}

ui::ImageModel BrowserFrameViewChromeOS::GetFaviconForTabIconView() {
  views::WidgetDelegate* delegate = browser_widget()->widget_delegate();
  return delegate ? delegate->GetWindowIcon() : ui::ImageModel();
}

void BrowserFrameViewChromeOS::OnWindowDestroying(aura::Window* window) {
  DCHECK(window_observation_.IsObserving());
  window_observation_.Reset();
  display_observer_.reset();
}

void BrowserFrameViewChromeOS::OnWindowPropertyChanged(aura::Window* window,
                                                       const void* key,
                                                       intptr_t old) {
  // ChromeOS has rounded windows for certain window states. If these states
  // changes, we need to update the rounded corners of the frame associate with
  // the `window`accordingly.
  if (key == chromeos::kWindowHasRoundedCornersKey) {
    UpdateWindowRoundedCorners();
    if (GetProperty(chromeos::kWindowHasRoundedCornersKey) != old) {
      if (auto* const browser_view = GetBrowserView()) {
        browser_view->InvalidateLayout();
      }
    }
  }

  if (key == aura::client::kShowStateKey) {
    bool enter_fullscreen = window->GetProperty(aura::client::kShowStateKey) ==
                            ui::mojom::WindowShowState::kFullscreen;
    bool exit_fullscreen = static_cast<ui::mojom::WindowShowState>(old) ==
                           ui::mojom::WindowShowState::kFullscreen;

    // May have to hide caption buttons while in fullscreen mode, or show them
    // when exiting fullscreen.
    if (enter_fullscreen || exit_fullscreen) {
      ResetWindowControls();
    }

    // The client view (in particular the tab strip) has different layout in
    // restored vs. maximized/fullscreen. Invalidate the layout because the
    // window bounds may not have changed. https://crbug.com/40230996
    if (browser_widget()->client_view()) {
      browser_widget()->client_view()->InvalidateLayout();
    }
  }

  if (key == chromeos::kWindowStateTypeKey) {
    // Update window controls when window state changes as whether or not these
    // are shown can depend on the window state (e.g. hiding the caption buttons
    // in non-immersive full screen mode, see crbug.com/40228932).
    ResetWindowControls();

    // Update the window controls if we are entering or exiting float state.
    const bool is_floated = IsFloated();
    const bool was_floated = static_cast<chromeos::WindowStateType>(old) ==
                             chromeos::WindowStateType::kFloated;

    if (is_floated != was_floated) {
      frame_header_->OnFloatStateChanged();
    }

    const bool is_fullscreen = browser_widget()->IsFullscreen();
    const bool was_fullscreen = chromeos::IsFullscreenOrPinnedWindowStateType(
        static_cast<chromeos::WindowStateType>(old));
    // Additionally updates immersive mode when the state is transitioning
    // between non fullscreen states such as maximzied, snapped or float. When
    // switching to or from the fullscreen states, or the state change between
    // fullscreen states (fullscreen <> pinneed), the immersive mode is updated
    // in `BrowserView::FullscreenStateChanged`.
    if (!is_fullscreen && !was_fullscreen) {
      GetImmersiveModeController()->SetEnabled(
          ShouldEnableImmersiveModeController());
    }

    return;
  }

  if (key == chromeos::kIsShowingInOverviewKey) {
    OnAddedToOrRemovedFromOverview();
    return;
  }

  if (key == aura::client::kShowStateKey) {
    frame_header_->OnShowStateChanged(
        window->GetProperty(aura::client::kShowStateKey));
  } else if (key == chromeos::kFrameRestoreLookKey) {
    frame_header_->view()->InvalidateLayout();
  }
}

void BrowserFrameViewChromeOS::OnImmersiveFullscreenEntered() {
  ResetWindowControls();
  auto* container = GetBrowserView()->top_container();
  container->AddChildViewAt(caption_button_container_.get(), 0);
  if (profile_indicator_icon_) {
    container->AddChildView(profile_indicator_icon_.get());
  }
}

void BrowserFrameViewChromeOS::OnImmersiveFullscreenExited() {
  ResetWindowControls();
  AddChildViewAt(caption_button_container_.get(), 0);
  if (profile_indicator_icon_) {
    AddChildView(profile_indicator_icon_.get());
  }

  OnImmersiveRevealEnded();
}

void BrowserFrameViewChromeOS::OnAppUpdate(const apps::AppUpdate& update) {
  BrowserWindowInterface* browser = GetBrowserView()->browser();

  if (!web_app::AppBrowserController::From(browser) ||
      web_app::AppBrowserController::From(browser)->app_id() !=
          update.AppId() ||
      !caption_button_container_) {
    return;
  }

  caption_button_container_->SetCloseButtonEnabled(
      update.AllowClose().value_or(true));
}

void BrowserFrameViewChromeOS::OnAppRegistryCacheWillBeDestroyed(
    apps::AppRegistryCache* cache) {
  app_registry_cache_observation_.Reset();
}

bool BrowserFrameViewChromeOS::ShouldEnableImmersiveModeController() const {
  // Do not support immersive mode in kiosk.
  if (chromeos::IsKioskSession()) {
    return false;
  }

  const aura::Window* frame_window = GetFrameWindow();
  if (!frame_window) {
    return false;
  }

  if (IsLockedFullscreen() &&
      (!frame_window->GetProperty(chromeos::kUseImmersiveInTrustedPinned))) {
    return false;
  }
  if (display::Screen::Get()->InTabletMode() &&
      (IsSnapped() || browser_widget()->IsMaximized())) {
    // Snapped or maximized browser windows that doesn't have tabstrip uses
    // immersive frame to hide frame in tablet mode.
    return !GetBrowserView()->GetSupportsTabStrip();
  }

  const auto* fullscreen_controller = GetBrowserView()
                                          ->browser()
                                          ->GetFeatures()
                                          .exclusive_access_manager()
                                          ->fullscreen_controller();
  // For other scnarios, use immersive if the browser is in fullscreen, and it
  // is NOT requested via extension or HTML API `requestFullscreen()`.
  return browser_widget()->IsFullscreen() &&
         !fullscreen_controller->IsExtensionFullscreenOrPending() &&
         fullscreen_controller->IsFullscreenForBrowser();
}

// static
bool BrowserFrameViewChromeOS::ShouldShowAvatarForTesting(
    aura::Window* window) {
  return ShouldShowAvatar(window);
}

bool BrowserFrameViewChromeOS::IsLockedFullscreen() const {
  return ash::WindowState::Get(browser_widget()->GetNativeWindow())
      ->IsLockedFullscreen();
}

void BrowserFrameViewChromeOS::PaintAsActiveChanged() {
  BrowserFrameView::PaintAsActiveChanged();

  UpdateProfileIcons();

  frame_header_->SetPaintAsActive(ShouldPaintAsActive());
}

void BrowserFrameViewChromeOS::AddedToWidget() {
  if (highlight_border_overlay_ ||
      !GetWidget()->GetNativeWindow()->GetProperty(
          chromeos::kShouldHaveHighlightBorderOverlay)) {
    return;
  }

  highlight_border_overlay_ = std::make_unique<HighlightBorderOverlay>(
      GetWidget(),
      std::make_unique<chromeos::DefaultHighlightBorderOverlayDelegate>());
}

BrowserFrameViewChromeOS::BoundsAndMargins
BrowserFrameViewChromeOS::GetCaptionButtonBounds() const {
  return BoundsAndMargins{GetShowCaptionButtonsWhenNotInOverview()
                              ? gfx::RectF(caption_button_container_->bounds())
                              : gfx::RectF()};
}

bool BrowserFrameViewChromeOS::GetShowCaptionButtons() const {
  if (GetOverviewMode()) {
    return false;
  }

  return GetShowCaptionButtonsWhenNotInOverview();
}

bool BrowserFrameViewChromeOS::GetShowCaptionButtonsWhenNotInOverview() const {
  const aura::Window* frame_window = GetFrameWindow();
  if (!frame_window) {
    return true;
  }

  // Show the caption buttons if an immersive mode is enabled for trusted pined
  // state. This is to show the three dot menu which is a part of caption button
  // container, rather than showing buttons. Only relevant for non-web browser
  // scenarios.
  if (IsLockedFullscreen() &&
      frame_window->GetProperty(chromeos::kUseImmersiveInTrustedPinned)) {
    return true;
  }

  if (GetHideCaptionButtonsForFullscreen()) {
    return false;
  }

  // Show the caption buttons for packaged apps which support immersive mode.
  if (UsePackagedAppHeaderStyle(GetBrowserView()->browser())) {
    return true;
  }

  // Browsers in tablet mode still show their caption buttons in float state.
  return !display::Screen::Get()->InTabletMode() || IsFloated();
}

int BrowserFrameViewChromeOS::GetToolbarLeftInset() const {
  // Include padding on left and right of icon.
  return profile_indicator_icon_
             ? kProfileIndicatorPadding * 2 + profile_indicator_icon_->width()
             : 0;
}

int BrowserFrameViewChromeOS::GetTabStripLeftInset() const {
  // Include padding on left of icon.
  // The tab strip has its own 'padding' to the right of the icon.
  return profile_indicator_icon_
             ? kProfileIndicatorPadding + profile_indicator_icon_->width()
             : 0;
}

int BrowserFrameViewChromeOS::GetTabStripRightInset() const {
  int inset = 0;
  if (GetShowCaptionButtonsWhenNotInOverview()) {
    inset += caption_button_container_->GetPreferredSize().width();
  }
  return inset;
}

bool BrowserFrameViewChromeOS::GetShouldPaint() const {
  // We need to paint when the top-of-window views are revealed in immersive
  // fullscreen.
  auto* const immersive_mode_controller = GetImmersiveModeController();
  if (immersive_mode_controller->IsEnabled()) {
    return immersive_mode_controller->IsRevealed();
  }

  return !browser_widget()->IsFullscreen();
}

void BrowserFrameViewChromeOS::OnAddedToOrRemovedFromOverview() {
  const bool should_show_caption_buttons = GetShowCaptionButtons();
  caption_button_container_->SetVisible(should_show_caption_buttons);
  if (GetBrowserView()->GetIsWebAppType()) {
    // The WebAppFrameToolbarView is part of the BrowserView, so make sure the
    // BrowserView is re-layed out to take into account these changes.
    GetBrowserView()->InvalidateLayout();
  }
}

std::unique_ptr<chromeos::FrameHeader>
BrowserFrameViewChromeOS::CreateFrameHeader() {
  std::unique_ptr<chromeos::FrameHeader> header;
  BrowserWindowInterface* browser = GetBrowserView()->browser();
  if (!UsePackagedAppHeaderStyle(browser)) {
    header = std::make_unique<BrowserFrameHeaderChromeOS>(
        browser_widget(), this, this, caption_button_container_);
  } else {
    header = std::make_unique<chromeos::DefaultFrameHeader>(
        browser_widget(), this, caption_button_container_);
  }

  header->SetLeftHeaderView(
      GetShowProfileIndicatorIcon() && profile_indicator_icon_
          ? static_cast<views::View*>(profile_indicator_icon_.get())
          : static_cast<views::View*>(window_icon_.get()));
  return header;
}

void BrowserFrameViewChromeOS::UpdateTopViewInset() {
  // In immersive fullscreen mode, the top view inset property should be 0.
  const bool immersive = GetImmersiveModeController()->IsEnabled();
  const bool tab_strip_visible = GetBrowserView()->GetTabStripVisible();
  const int inset = (tab_strip_visible || immersive ||
                     (AppIsPwaWithUnframedDisplayMode() &&
                      GetBrowserView()->IsUnframedModeEnabled()))
                        ? 0
                        : GetTopInset(/*restored=*/false);
  browser_widget()->GetNativeWindow()->SetProperty(aura::client::kTopViewInset,
                                                   inset);
}

bool BrowserFrameViewChromeOS::GetShowProfileIndicatorIcon() const {
  // We only show the profile indicator for the teleported browser windows
  // between multi-user sessions. Note that you can't teleport an incognito
  // window.
  BrowserWindowInterface* browser = GetBrowserView()->browser();
  if (browser->GetProfile()->IsIncognitoProfile()) {
    return false;
  }

  if (browser->GetType() == BrowserWindowInterface::Type::TYPE_POPUP) {
    return false;
  }

  return ShouldShowAvatar(GetBrowserView()->GetNativeWindow());
}

void BrowserFrameViewChromeOS::UpdateProfileIcons() {
  // `profile_indicator_icon_` is parented to browser's top container in
  // immersive-view.
  auto profile_icon_parent = [this]() -> views::View* {
    auto* immersive_controller = GetImmersiveModeController();
    if (immersive_controller && immersive_controller->IsEnabled()) {
      return GetBrowserView()->top_container();
    }

    return this;
  };

  if (GetShowProfileIndicatorIcon()) {
    bool needs_layout = !profile_indicator_icon_;
    if (!profile_indicator_icon_) {
      profile_indicator_icon_ = profile_icon_parent()->AddChildView(
          std::make_unique<ProfileIndicatorIcon>());
    }

    gfx::Image image(
        GetAvatarImageForContext(GetBrowserView()->browser()->GetProfile()));
    profile_indicator_icon_->SetSize(image.Size());
    profile_indicator_icon_->SetPreferredSize(image.Size());
    profile_indicator_icon_->SetIcon(image);

    frame_header_->SetLeftHeaderView(profile_indicator_icon_);
    if (window_icon_) {
      window_icon_->SetVisible(false);
    }

    if (needs_layout) {
      // Adding a child does not invalidate the layout.
      RelayoutBrowserWindow();
    }
  } else if (profile_indicator_icon_) {
    profile_icon_parent()->RemoveChildViewT(
        std::exchange(profile_indicator_icon_, nullptr));

    frame_header_->SetLeftHeaderView(window_icon_);
    if (window_icon_) {
      window_icon_->SetVisible(true);
    }
    RelayoutBrowserWindow();
  }
}

gfx::RoundedCornersF BrowserFrameViewChromeOS::GetWindowRoundedCorners() const {
  if (aura::Window* const window = GetWidget()->GetNativeWindow()) {
    if (auto* const window_state = ash::WindowState::Get(window)) {
      return window_state->GetWindowRoundedCorners();
    }
  }
  return gfx::RoundedCornersF();
}

void BrowserFrameViewChromeOS::UpdateWindowRoundedCorners() {
  DCHECK(GetWidget());

  const gfx::RoundedCornersF window_radii = GetWindowRoundedCorners();

  CHECK_EQ(window_radii.upper_left(), window_radii.upper_right());
  frame_header_->SetHeaderCornerRadius(window_radii.upper_left());

  if (GetBrowserView()->IsWindowControlsOverlayEnabled()) {
    // With window controls overlay enabled, the caption_button_container is
    // drawn above the client view. The container has a background that extends
    // over the curvature of the top-right corner, requiring its rounding.
    caption_button_container_->layer()->SetRoundedCornerRadius(
        gfx::RoundedCornersF(0, window_radii.upper_right(), 0, 0));
    caption_button_container_->layer()->SetIsFastRoundedCorner(/*enable=*/true);
  }

  GetWidget()->client_view()->UpdateWindowRoundedCorners(window_radii);
}

void BrowserFrameViewChromeOS::MaybeAddAppIconToLayoutParams(
    BrowserLayoutParams& params) const {
  if (GetBrowserView()->ShouldShowWindowIcon() && window_icon_) {
    const auto icon_bounds = window_icon_->bounds();
    const float icon_right = icon_bounds.right();
    const float icon_bottom = icon_bounds.bottom();

    // The icon may extend the size of the exclusion area.
    params.leading_exclusion.content.set_width(
        std::max(params.leading_exclusion.content.width(), icon_right));
    params.leading_exclusion.content.set_height(
        std::max(params.leading_exclusion.content.height(), icon_bottom));

    // Default space between window icon and title text for ChromeOS.
    // (See `kTitleIconOffsetX` in chromeos/ui/frame/frame_header.cc).
    params.leading_exclusion.horizontal_padding = 5.f;
  }
}

void BrowserFrameViewChromeOS::LayoutProfileIndicator() {
  CHECK(profile_indicator_icon_);
  const int frame_height =
      GetBrowserView()->ShouldDrawVerticalTabStrip() &&
              GetBrowserView()->toolbar()
          ? GetBrowserView()->toolbar()->height()
          : GetTopInset(false) +
                GetClientFrameElementInfo().tabstrip_preferred_height;

  if (frame_height == 0) {
    // When transitioning into immersive-fullscreen, frame height is zero for
    // initial layout passes.
    return;
  }
  profile_indicator_icon_->SetPosition(
      gfx::Point(kProfileIndicatorPadding,
                 (frame_height - profile_indicator_icon_->height()) / 2));
  profile_indicator_icon_->SetVisible(true);

  // The layout size is set along with the image.
  DCHECK_LE(profile_indicator_icon_->height(), frame_height);
}

bool BrowserFrameViewChromeOS::GetOverviewMode() const {
  const aura::Window* frame_window = GetFrameWindow();
  return frame_window &&
         frame_window->GetProperty(chromeos::kIsShowingInOverviewKey);
}

bool BrowserFrameViewChromeOS::GetHideCaptionButtonsForFullscreen() const {
  if (!browser_widget()->IsFullscreen()) {
    return false;
  }

  auto* const immersive_controller = GetImmersiveModeController();

  // In fullscreen view, but not in immersive mode. Hide the caption buttons.
  return !immersive_controller || !immersive_controller->IsEnabled();
}

void BrowserFrameViewChromeOS::OnUpdateFrameColor() {
  aura::Window* window = browser_widget()->GetNativeWindow();
  window->SetProperty(chromeos::kFrameActiveColorKey,
                      GetFrameColor(BrowserFrameActiveState::kActive));
  window->SetProperty(chromeos::kFrameInactiveColorKey,
                      GetFrameColor(BrowserFrameActiveState::kInactive));

  frame_header_->UpdateFrameColors();
}

bool BrowserFrameViewChromeOS::IsFloated() const {
  return ash::WindowState::Get(browser_widget()->GetNativeWindow())
      ->IsFloated();
}

bool BrowserFrameViewChromeOS::IsSnapped() const {
  return ash::WindowState::Get(browser_widget()->GetNativeWindow())
      ->IsSnapped();
}

const aura::Window* BrowserFrameViewChromeOS::GetFrameWindow() const {
  return const_cast<BrowserFrameViewChromeOS*>(this)->GetFrameWindow();
}

aura::Window* BrowserFrameViewChromeOS::GetFrameWindow() {
  return browser_widget()->GetNativeWindow();
}

ImmersiveModeController* BrowserFrameViewChromeOS::GetImmersiveModeController()
    const {
  return ImmersiveModeController::From(GetBrowserView()->browser());
}

void BrowserFrameViewChromeOS::RelayoutBrowserWindow() {
  InvalidateLayout();
  if (GetBrowserView()->GetIsWebAppType()) {
    // We must invalidate the BrowserView layout as it is responsible for
    // painting the window title in web apps (See
    // `BrowserView::web_app_window_title_`).
    GetBrowserView()->InvalidateLayout();
  }
  if (views::View* root_view = browser_widget()->GetRootView()) {
    root_view->DeprecatedLayoutImmediately();
  }
}

BEGIN_METADATA(BrowserFrameViewChromeOS)
ADD_READONLY_PROPERTY_METADATA(bool, ShowCaptionButtons)
ADD_READONLY_PROPERTY_METADATA(bool, ShowCaptionButtonsWhenNotInOverview)
ADD_READONLY_PROPERTY_METADATA(int, ToolbarLeftInset)
ADD_READONLY_PROPERTY_METADATA(int, TabStripLeftInset)
ADD_READONLY_PROPERTY_METADATA(int, TabStripRightInset)
ADD_READONLY_PROPERTY_METADATA(bool, ShouldPaint)
ADD_READONLY_PROPERTY_METADATA(bool, ShowProfileIndicatorIcon)
ADD_READONLY_PROPERTY_METADATA(bool, OverviewMode)

END_METADATA
