// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/wm/overview/overview_utils.h"

#include <utility>

#include "ash/accessibility/accessibility_controller.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/root_window_controller.h"
#include "ash/screen_util.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_layout_manager.h"
#include "ash/shell.h"
#include "ash/wm/mru_window_tracker.h"
#include "ash/wm/overview/cleanup_animation_observer.h"
#include "ash/wm/overview/delayed_animation_observer_impl.h"
#include "ash/wm/overview/overview_constants.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_grid.h"
#include "ash/wm/overview/overview_item.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/scoped_overview_animation_settings.h"
#include "ash/wm/snap_group/snap_group_controller.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/splitview/split_view_overview_session.h"
#include "ash/wm/splitview/split_view_types.h"
#include "ash/wm/splitview/split_view_utils.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_transient_descendant_iterator.h"
#include "ash/wm/window_util.h"
#include "ash/wm/wm_event.h"
#include "ash/wm/work_area_insets.h"
#include "chromeos/ui/frame/caption_buttons/snap_controller.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/display/screen.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/wm/core/coordinate_conversion.h"
#include "ui/wm/core/scoped_animation_disabler.h"
#include "ui/wm/core/window_animations.h"

namespace ash {

constexpr int kBirchBarHotseatSpacing = 10;

bool IsInOverviewSession() {
  OverviewController* overview_controller = OverviewController::Get();
  return overview_controller && overview_controller->InOverviewSession();
}

OverviewSession* GetOverviewSession() {
  OverviewController* overview_controller = OverviewController::Get();
  return overview_controller && overview_controller->InOverviewSession()
             ? overview_controller->overview_session()
             : nullptr;
}

bool CanCoverAvailableWorkspace(aura::Window* window) {
  if (!window) {
    return false;
  }
  SplitViewController* split_view_controller = SplitViewController::Get(window);
  if (split_view_controller->InSplitViewMode())
    return split_view_controller->CanKeepCurrentSnapRatio(window);
  return WindowState::Get(window)->IsMaximizedOrFullscreenOrPinned();
}

void FadeInAndTransformWidgetToOverview(views::Widget* widget,
                                        const gfx::Transform& target_transform,
                                        OverviewAnimationType animation_type,
                                        bool observe) {
  aura::Window* window = widget->GetNativeWindow();
  auto* window_layer = window->layer();
  const bool animate_opacity = window_layer->GetTargetOpacity() != 1;
  const bool animate_transform =
      window_layer->GetTargetTransform() != target_transform;
  if (!animate_opacity && !animate_transform) {
    return;
  }

  // Fade in the widget from its current opacity.
  ScopedOverviewAnimationSettings scoped_overview_animation_settings(
      animation_type, window);
  if (animate_opacity) {
    window_layer->SetOpacity(1.0f);
  }

  if (animate_transform) {
    window_layer->SetTransform(target_transform);
  }

  if (observe) {
    auto enter_observer = std::make_unique<EnterAnimationObserver>();
    scoped_overview_animation_settings.AddObserver(enter_observer.get());
    OverviewController::Get()->AddEnterAnimationObserver(
        std::move(enter_observer));
  }
}

void FadeInWidgetToOverview(views::Widget* widget,
                            OverviewAnimationType animation_type,
                            bool observe) {
  FadeInAndTransformWidgetToOverview(widget,
                                     widget->GetLayer()->GetTargetTransform(),
                                     animation_type, observe);
}

void PrepareWidgetForShutdownAnimation(views::Widget* widget) {
  // The widget should no longer process events at this point.
  widget->SetVisibilityChangedAnimationsEnabled(false);
  widget->widget_delegate()->SetCanActivate(false);
  widget->GetNativeWindow()->SetEventTargetingPolicy(
      aura::EventTargetingPolicy::kNone);
  widget->GetContentsView()->SetCanProcessEventsWithinSubtree(false);
  widget->GetFocusManager()->set_shortcut_handling_suspended(true);
}

void FadeOutWidgetFromOverview(std::unique_ptr<views::Widget> widget,
                               OverviewAnimationType animation_type) {
  PrepareWidgetForShutdownAnimation(widget.get());

  // The overview controller may be nullptr on shutdown.
  OverviewController* controller = OverviewController::Get();
  if (!controller) {
    widget->SetOpacity(0.f);
    return;
  }

  // Fade out the widget from its current opacity. This animation continues past
  // the lifetime of overview mode items.
  ScopedOverviewAnimationSettings animation_settings(animation_type,
                                                     widget->GetNativeWindow());
  // CleanupAnimationObserver will delete itself (and the widget) when the
  // opacity animation is complete. Ownership over the observer is passed to the
  // overview controller which has longer lifetime so that animations can
  // continue even after the overview mode is shut down.
  views::Widget* widget_ptr = widget.get();
  auto observer = std::make_unique<CleanupAnimationObserver>(std::move(widget));
  animation_settings.AddObserver(observer.get());
  controller->AddExitAnimationObserver(std::move(observer));
  widget_ptr->SetOpacity(0.f);
}

void ImmediatelyCloseWidgetOnExit(std::unique_ptr<views::Widget> widget) {
  widget->GetNativeWindow()->SetProperty(aura::client::kAnimationsDisabledKey,
                                         true);
  widget->Close();
  widget.reset();
}

gfx::RectF GetUnionScreenBoundsForWindow(aura::Window* window) {
  gfx::RectF bounds;
  for (auto* window_iter :
       window_util::GetVisibleTransientTreeIterator(window)) {
    // Ignore other window types when computing bounding box of overview target
    // item.
    if (window_iter != window &&
        window_iter->GetType() != aura::client::WINDOW_TYPE_NORMAL) {
      continue;
    }
    gfx::RectF target_bounds(window_iter->GetTargetBounds());
    wm::TranslateRectToScreen(window_iter->parent(), &target_bounds);
    bounds.Union(target_bounds);
  }

  return bounds;
}

OverviewItemFillMode GetOverviewItemFillMode(const gfx::Size& size) {
  if (size.width() > size.height() * kExtremeWindowRatioThreshold) {
    return OverviewItemFillMode::kLetterBoxed;
  }

  if (size.height() > size.width() * kExtremeWindowRatioThreshold) {
    return OverviewItemFillMode::kPillarBoxed;
  }

  return OverviewItemFillMode::kNormal;
}

OverviewItemFillMode GetOverviewItemFillModeForWindow(aura::Window* window) {
  SnapGroupController* snap_group_controller = SnapGroupController::Get();
  if (!snap_group_controller ||
      !snap_group_controller->GetSnapGroupForGivenWindow(window)) {
    return GetOverviewItemFillMode(window->bounds().size());
  }

  return OverviewItemFillMode::kNormal;
}

void MaximizeIfSnapped(aura::Window* window) {
  auto* window_state = WindowState::Get(window);
  if (window_state && window_state->IsSnapped()) {
    wm::ScopedAnimationDisabler disabler(window);
    WMEvent event(WM_EVENT_MAXIMIZE);
    window_state->OnWMEvent(&event);
  }
}

gfx::Rect GetGridBoundsInScreen(aura::Window* target_root) {
  return GetGridBoundsInScreen(target_root,
                               /*window_dragging_state=*/std::nullopt,
                               /*account_for_hotseat=*/true);
}

gfx::Rect GetGridBoundsInScreen(
    aura::Window* target_root,
    std::optional<SplitViewDragIndicators::WindowDraggingState>
        window_dragging_state,
    bool account_for_hotseat) {
  auto* split_view_controller = SplitViewController::Get(target_root);
  SplitViewController::State state = split_view_controller->state();

  // If we are in splitview mode already just use the given state, otherwise
  // convert `window_dragging_state` to a split view state. Note this will
  // override `state` from `split_view_overview_session` if there is any.
  if (!split_view_controller->InSplitViewMode() && window_dragging_state) {
    switch (*window_dragging_state) {
      case SplitViewDragIndicators::WindowDraggingState::kToSnapPrimary:
        state = SplitViewController::State::kPrimarySnapped;
        break;
      case SplitViewDragIndicators::WindowDraggingState::kToSnapSecondary:
        state = SplitViewController::State::kSecondarySnapped;
        break;
      default:
        break;
    }
  }

  const gfx::Rect work_area =
      WorkAreaInsets::ForWindow(target_root)->ComputeStableWorkArea();
  gfx::Rect bounds = work_area;
  std::optional<SnapPosition> opposite_position;

  // We should show partial overview for the following use cases:
  // 1. In tablet split view mode with one window snapped.
  // 2. In clamshell `SplitViewOverviewSession`.
  if (auto* split_view_overview_session =
          RootWindowController::ForWindow(target_root)
              ->split_view_overview_session()) {
    aura::Window* snapped_window = split_view_overview_session->window();
    gfx::Rect target_bounds_in_screen(snapped_window->GetTargetBounds());
    WindowState* window_state = WindowState::Get(snapped_window);
    CHECK(window_state->IsSnapped());
    opposite_position = window_state->GetStateType() ==
                                chromeos::WindowStateType::kPrimarySnapped
                            ? SnapPosition::kSecondary
                            : SnapPosition::kPrimary;
    wm::ConvertRectToScreen(target_root, &target_bounds_in_screen);
    bounds.Subtract(target_bounds_in_screen);
  } else {
    const bool account_for_divider_width =
        display::Screen::Get()->InTabletMode();
    switch (state) {
      case SplitViewController::State::kPrimarySnapped:
        bounds = split_view_controller->GetSnappedWindowBoundsInScreen(
            SnapPosition::kSecondary,
            /*window_for_minimum_size=*/nullptr, chromeos::kDefaultSnapRatio,
            account_for_divider_width);
        opposite_position = SnapPosition::kSecondary;
        break;
      case SplitViewController::State::kSecondarySnapped:
        bounds = split_view_controller->GetSnappedWindowBoundsInScreen(
            SnapPosition::kPrimary,
            /*window_for_minimum_size=*/nullptr, chromeos::kDefaultSnapRatio,
            account_for_divider_width);
        opposite_position = SnapPosition::kPrimary;
        break;
      case SplitViewController::State::kNoSnap:
        break;
      case SplitViewController::State::kBothSnapped:
        // When this function is called, SplitViewController should have
        // already handled the state change.
        NOTREACHED();
    }
  }

  // Hotseat overlaps the work area / split view bounds when extended, but in
  // some cases we don't want its bounds in our calculations. When the forest
  // features are enabled, we want to exclude the hotseat bounds when it is
  // shown in home launcher.
  if (account_for_hotseat && display::Screen::Get()->InTabletMode()) {
    const HotseatState hotseat_state =
        Shelf::ForWindow(target_root)->shelf_layout_manager()->hotseat_state();

    const bool show_home_launcher =
        hotseat_state == HotseatState::kShownHomeLauncher;

    const int hotseat_bottom_inset =
        ShelfConfig::Get()->GetHotseatSize(HotseatDensity::kNormal) +
        ShelfConfig::Get()->hotseat_bottom_padding();

    if (show_home_launcher) {
      // If the home launcher is shown, add some extra spacing between the birch
      // bar and the hotseat. Subtract the in app shelf size since it is already
      // factored in the work area calculations.
      bounds.Inset(
          gfx::Insets::TLBR(0, 0,
                            hotseat_bottom_inset + kBirchBarHotseatSpacing -
                                ShelfConfig::Get()->in_app_shelf_size(),
                            0));
    }
  }

  // Clamp the bounds of the overview grid such that it doesn't go below
  // `kOverviewGridClampThresholdRatio` of the work area length.
  const bool horizontal = IsLayoutHorizontal(target_root);
  const int min_length = (horizontal ? work_area.width() : work_area.height()) *
                         kOverviewGridClampThresholdRatio;
  const int current_length = horizontal ? bounds.width() : bounds.height();

  if (current_length > min_length)
    return bounds;

  // Clamp bounds' corresponding length to the minimum length.
  if (horizontal)
    bounds.set_width(min_length);
  else
    bounds.set_height(min_length);

  // These changes below are crucial for better visualization and help
  // preventing crashes when dragging the snapped window towards the edge. In
  // this case, the overview components will become too small to allow any
  // gradient painting on desk bar or applying shadows. Please ensure to go
  // through the bounds update below when one window is snapped in overview both
  // in clamshell and tablet mode. See the regression behavior in
  // http://b/324478757.
  if (opposite_position &&
      IsPhysicallyLeftOrTop(*opposite_position, target_root)) {
    // If we are shifting to the left or top we need to update the origin as
    // well.
    const int offset = min_length - current_length;
    bounds.Offset(horizontal ? gfx::Vector2d(-offset, 0)
                             : gfx::Vector2d(0, -offset));
  }

  return bounds;
}

std::optional<gfx::RectF> GetSplitviewBoundsMaintainingAspectRatio() {
  if (!ShouldAllowSplitView()) {
    return std::nullopt;
  }
  if (!display::Screen::Get()->InTabletMode()) {
    return std::nullopt;
  }
  auto* overview_session = OverviewController::Get()->overview_session();
  DCHECK(overview_session);
  aura::Window* root_window = Shell::GetPrimaryRootWindow();
  DCHECK(overview_session->GetGridWithRootWindow(root_window)
             ->split_view_drag_indicators());
  auto window_dragging_state =
      overview_session->GetGridWithRootWindow(root_window)
          ->split_view_drag_indicators()
          ->current_window_dragging_state();
  if (!SplitViewController::Get(root_window)->InSplitViewMode() &&
      SplitViewDragIndicators::GetSnapPosition(window_dragging_state) ==
          SnapPosition::kNone) {
    return std::nullopt;
  }

  // The hotseat bounds do not affect splitview after a window is snapped, so
  // the aspect ratio should reflect it and not worry about the hotseat.
  return gfx::RectF(GetGridBoundsInScreen(root_window, window_dragging_state,
                                          /*account_for_hotseat=*/false));
}

bool ShouldUseTabletModeGridLayout() {
  return display::Screen::Get()->InTabletMode();
}

gfx::Rect ToStableSizeRoundedRect(const gfx::RectF& rect) {
  return gfx::Rect(gfx::ToRoundedPoint(rect.origin()),
                   gfx::ToRoundedSize(rect.size()));
}

bool IsEligibleForDraggingToSnapInOverview(OverviewItemBase* item) {
  return (item->GetWindows().size() == 1u) && ShouldAllowSplitView();
}

void SetWindowsVisibleDuringItemDragging(const aura::Window::Windows& windows,
                                         bool visible,
                                         bool animate) {
  float new_opacity = visible ? 1.f : 0.f;
  for (aura::Window* window : windows) {
    ui::Layer* layer = window->layer();
    if (layer->GetTargetOpacity() == new_opacity) {
      continue;
    }

    if (animate) {
      ScopedOverviewAnimationSettings settings(
          OVERVIEW_ANIMATION_OPACITY_ON_WINDOW_DRAG, window);
      layer->SetOpacity(new_opacity);
    } else {
      layer->SetOpacity(new_opacity);
    }
  }
}

ui::ImageModel CreateIconForMenuItem(const gfx::VectorIcon& icon) {
  constexpr ui::ColorId kMenuIconColorId = cros_tokens::kCrosSysOnSurface;
  constexpr int kMenuIconSize = 20;
  return ui::ImageModel::FromVectorIcon(icon, kMenuIconColorId, kMenuIconSize);
}

}  // namespace ash
