// Copyright 2023 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/compose/compose_dialog_view.h"

#include <vector>

#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu.h"
#include "components/compose/core/browser/config.h"
#include "components/renderer_context_menu/context_menu_delegate.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/menus/simple_menu_model.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/controls/webview/webview.h"
#include "ui/views/view_class_properties.h"

namespace {

gfx::Rect FallbackAtBottomShiftUpToPermanentPosition(
    gfx::Rect best_location,
    const gfx::Rect& screen_work_area,
    const gfx::Size& widget_size) {
  // Make sure all sizes of the view will fit in the work area
  best_location.AdjustToFit(screen_work_area);

  // Now make best_location reflect the actual size of the widget.
  best_location.set_size(widget_size);

  return best_location;
}

gfx::Rect FallbackAtBottomShiftMinimumForVisibility(gfx::Rect best_location) {
  // Just use the initial location anchored at bottom left directly.
  return best_location;
}

gfx::Rect FallbackCenterOnFormFieldStrategy(const gfx::Rect& screen_work_are,
                                            const gfx::Size& widget_size,
                                            const gfx::Rect& anchor_rect) {
  gfx::Rect widget_rect;
  widget_rect.set_size(widget_size);
  gfx::Vector2d translation = anchor_rect.CenterPoint().OffsetFromOrigin() -
                              widget_rect.CenterPoint().OffsetFromOrigin();

  widget_rect.Offset(translation);
  return widget_rect;
}

}  // namespace

DEFINE_ELEMENT_IDENTIFIER_VALUE(kComposeWebviewElementId);
DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ComposeDialogView, kComposeDialogId);

// static
gfx::Rect ComposeDialogView::CalculateBubbleBounds(
    gfx::Rect screen_work_area,
    gfx::Size widget_size,
    gfx::Rect anchor_bounds,
    std::optional<gfx::Rect> parent_bounds) {
  // Widget can't be smaller than a minimum size.
  widget_size.SetToMax({kComposeMaxDialogWidthPx, kComposeMinDialogHeightPx});

  // We don't want to render anything within `padding` pixels of the edge of the
  // screen work area.
  screen_work_area.Inset(kComposeDialogWorkAreaPadding);

  // If the param to stay in the window bounds is true, and the window is large
  // enough, use that as the work area instead.
  if (compose::GetComposeConfig().stay_in_window_bounds &&
      parent_bounds.has_value()) {
    if ((widget_size.width() <= parent_bounds->width() &&
         widget_size.height() <= parent_bounds->height())) {
      screen_work_area = parent_bounds.value();
    }
  }

  // We don't want to render anything within `padding` pixels of the edge of the
  // anchor rect.  But we will if we have to (due to AdjustToFit below).
  anchor_bounds.Outset(kComposeDialogAnchorPadding);

  // Available space measures the distance from each side of the padded work
  // area to the edge of the padded anchor (plus padding).
  gfx::Insets available_space = screen_work_area.InsetsFrom(anchor_bounds);

  // Ideally we render at the bottom left of the anchor. If the dialog would be
  // offscreen, we reposition it.
  gfx::Rect best_location(
      anchor_bounds.bottom_left(),
      gfx::Size(kComposeMaxDialogWidthPx, kComposeMaxDialogHeightPx));

  bool space_below = available_space.bottom() >= kComposeMaxDialogHeightPx;
  bool space_above = available_space.top() >= kComposeMaxDialogHeightPx;

  // the position of the dialog is determined by finding a suitable location for
  // a maximum-sized ComposeDialogView, so that we know that the dialog doesn't
  // have to switch sides as it resizes. If the dialog is smaller than max, we
  // need to apply an alignment within that larger rectangle.

  if (!space_below) {
    // Not enough room in the preferred location. Try above the anchor.
    if (space_above) {
      // If it is laid out above the anchor rect then it
      // will be bottom-aligned.
      best_location.set_y(anchor_bounds.y() - widget_size.height());
    } else {
      // If not enough space above or below, try one of the following backup
      // strategies:

      switch (compose::GetComposeConfig().positioning_strategy) {
        case compose::DialogFallbackPositioningStrategy::kCenterOnAnchorRect:
          best_location = FallbackCenterOnFormFieldStrategy(
              screen_work_area, widget_size, anchor_bounds);
          break;
        case compose::DialogFallbackPositioningStrategy::kShiftUpUntilOnscreen:
          best_location =
              FallbackAtBottomShiftMinimumForVisibility(best_location);
          break;
        case compose::DialogFallbackPositioningStrategy::
            kShiftUpUntilMaxSizeIsOnscreen:
        default:
          best_location = FallbackAtBottomShiftUpToPermanentPosition(
              best_location, screen_work_area, widget_size);
          break;
      }
    }
  }

  // Always use the size that the WebUI wants to be in the end.
  best_location.set_size(widget_size);

  // Always remain completely on screen within the provided insets.
  best_location.AdjustToFit(screen_work_area);

  return best_location;
}

ComposeDialogView::~ComposeDialogView() = default;

ComposeDialogView::ComposeDialogView(
    View* anchor_view,
    std::unique_ptr<WebUIContentsWrapperT<ComposeUntrustedUI>> bubble_wrapper,
    const gfx::Rect& anchor_bounds,
    views::BubbleBorder::Arrow anchor_position)
    : WebUIBubbleDialogView(anchor_view,
                            bubble_wrapper->GetWeakPtr(),
                            anchor_bounds,
                            anchor_position),
      anchor_bounds_(anchor_bounds),
      bubble_wrapper_(std::move(bubble_wrapper)) {
  SetProperty(views::kElementIdentifierKey, kComposeDialogId);
  web_view()->SetProperty(views::kElementIdentifierKey,
                          kComposeWebviewElementId);
}

void ComposeDialogView::OnBeforeBubbleWidgetInit(
    views::Widget::InitParams* params,
    views::Widget* widget) const {
  WebUIBubbleDialogView::OnBeforeBubbleWidgetInit(params, widget);
#if BUILDFLAG(IS_LINUX)
  // In linux, windows may be clipped to their anchors' bounds,
  // resulting in visual errors, unless they use accelerated rendering. See
  // crbug.com/40912626 for details.
  params->use_accelerated_widget_override = true;
#endif
}

gfx::Rect ComposeDialogView::GetBubbleBounds() {
  gfx::Size widget_size = BubbleDialogDelegateView::GetBubbleBounds().size();

  std::optional<gfx::Rect> parent_bounds;
  if (GetWidget()->parent()) {
    parent_bounds = GetWidget()->parent()->GetWindowBoundsInScreen();
  }

  display::Display display = display::Screen::Get()->GetDisplayNearestView(
      GetAnchorView()->GetWidget()->GetNativeView());
  gfx::Rect screen_work_area = display.work_area();

  return CalculateBubbleBounds(screen_work_area, widget_size, anchor_bounds_,
                               parent_bounds);
}

bool ComposeDialogView::HandleContextMenu(
    content::RenderFrameHost& render_frame_host,
    const content::ContextMenuParams& params) {
  ContextMenuDelegate* menu_delegate = ContextMenuDelegate::FromWebContents(
      content::WebContents::FromRenderFrameHost(&render_frame_host));
  DCHECK(menu_delegate);

  menu_delegate->BuildMenuAsync(
      render_frame_host, params,
      base::BindOnce(&ComposeDialogView::OnBuildMenuComplete,
                     weak_ptr_factory_.GetWeakPtr()));
  return true;
}

void ComposeDialogView::OnBuildMenuComplete(
    std::unique_ptr<RenderViewContextMenuBase> menu) {
  if (!menu) {
    return;
  }
  // Remove everything that is not copy, paste, or cut or spellcheck
  // suggestions.
  constexpr int kMaxSpellingSuggestionsLimit = 5;
  constexpr int kSpellcheckSuggestionLast =
      IDC_SPELLCHECK_SUGGESTION_0 + kMaxSpellingSuggestionsLimit - 1;
  std::vector<int> command_ids;
  for (size_t index = 0; index < menu->menu_model().GetItemCount(); index++) {
    int command_id = menu->menu_model().GetCommandIdAt(index);
    if ((command_id < IDC_CONTENT_CONTEXT_COPY ||
         command_id > IDC_CONTENT_CONTEXT_PASTE_AND_MATCH_STYLE) &&
        (command_id < IDC_SPELLCHECK_SUGGESTION_0 ||
         command_id > kSpellcheckSuggestionLast) &&
        command_id != IDC_CONTENT_CONTEXT_INSPECTELEMENT && command_id > 0) {
      command_ids.push_back(command_id);
    }
  }

  for (int command_id : command_ids) {
    menu->RemoveMenuItem(command_id);
  }
  menu->RemoveAdjacentSeparators();

  // There's no method to remove the final separator if there is one, so we have
  // to hack around it.
  menu->RemoveSeparatorBeforeMenuItem(IDC_CONTENT_CONTEXT_INSPECTELEMENT);
  menu->RemoveMenuItem(IDC_CONTENT_CONTEXT_INSPECTELEMENT);

  // Only show the menu if there are items in it.
  if (menu->menu_model().GetItemCount() > 0) {
    context_menu_model_for_testing_ = &menu->menu_model();
    ContextMenuDelegate* menu_delegate =
        ContextMenuDelegate::FromWebContents(bubble_wrapper_->web_contents());
    DCHECK(menu_delegate);
    menu_delegate->ShowMenu(std::move(menu));
  }
}

base::WeakPtr<ComposeDialogView> ComposeDialogView::GetWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

BEGIN_METADATA(ComposeDialogView)
END_METADATA
