// Copyright 2021 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/image_editor/screenshot_flow.h"

#include <memory>

#include "base/files/file_path.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/supports_user_data.h"
#include "build/build_config.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/cursor/cursor.h"
#include "ui/compositor/layer_textured.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/events/event_target.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/native_ui_types.h"
#include "ui/gfx/render_text.h"
#include "ui/snapshot/snapshot.h"
#include "ui/views/background.h"

#if BUILDFLAG(IS_MAC)
#include "chrome/browser/image_editor/event_capture_mac.h"
#include "components/lens/lens_features.h"
#include "content/public/browser/render_view_host.h"
#include "ui/views/widget/widget.h"
#endif

#if defined(USE_AURA)
#include "ui/aura/window.h"
#include "ui/wm/core/window_util.h"
#endif

namespace image_editor {

// Colors for semitransparent overlay.
static constexpr SkColor kColorSemitransparentOverlayMask =
    SkColorSetARGB(0x9F, 0x00, 0x00, 0x00);
static constexpr SkColor kColorSemitransparentOverlayVisible =
    SkColorSetARGB(0x00, 0x00, 0x00, 0x00);
static constexpr SkColor kColorSelectionRect = SkColorSetRGB(0xEE, 0xEE, 0xEE);

// Minimum selection rect edge size to treat as a valid capture region.
static constexpr int kMinimumValidSelectionEdgePixels = 30;

ScreenshotCapturedData::ScreenshotCapturedData() = default;
ScreenshotCapturedData::~ScreenshotCapturedData() = default;

// UnderlyingWebContentsObserver monitors the WebContents and exits screen
// capture mode if a navigation occurs.
class ScreenshotFlow::UnderlyingWebContentsObserver
    : public content::WebContentsObserver {
 public:
  UnderlyingWebContentsObserver(content::WebContents* web_contents,
                                ScreenshotFlow* screenshot_flow)
      : content::WebContentsObserver(web_contents),
        screenshot_flow_(screenshot_flow) {}

  ~UnderlyingWebContentsObserver() override = default;

  UnderlyingWebContentsObserver(const UnderlyingWebContentsObserver&) = delete;
  UnderlyingWebContentsObserver& operator=(
      const UnderlyingWebContentsObserver&) = delete;

  // content::WebContentsObserver
  void PrimaryPageChanged(content::Page& page) override {
    // We only care to complete/cancel a capture if the capture mode is
    // currently active.
    if (screenshot_flow_->IsCaptureModeActive()) {
      screenshot_flow_->CompleteCapture(
          ScreenshotCaptureResultCode::USER_NAVIGATED_EXIT, gfx::Rect());
    }
  }

  // content::WebContentsObserver
  void FrameSizeChanged(content::RenderFrameHost* render_frame_host,
                        const gfx::Size& frame_size) override {
    // We only care to resize the UI overlay when it's visible to the user.
    if (screenshot_flow_->IsUIOverlayShown()) {
      screenshot_flow_->ResetUIOverlayBounds();
    }
  }

 private:
  raw_ptr<ScreenshotFlow> screenshot_flow_;
};

ScreenshotFlow::ScreenshotFlow(content::WebContents* web_contents)
    : content::WebContentsObserver(web_contents),
      web_contents_(web_contents->GetWeakPtr()) {
  weak_this_ = weak_factory_.GetWeakPtr();
}

ScreenshotFlow::~ScreenshotFlow() {
  RemoveUIOverlay();
}

void ScreenshotFlow::CreateAndAddUIOverlay() {
  if (screen_capture_layer_)
    return;
  web_contents_observer_ = std::make_unique<UnderlyingWebContentsObserver>(
      web_contents_.get(), this);
  screen_capture_layer_ = std::make_unique<ui::LayerTextured>();
  screen_capture_layer_->SetName("ScreenshotRegionSelectionLayer");
  screen_capture_layer_->SetFillsBoundsOpaquely(false);
  screen_capture_layer_->set_delegate(this);
#if BUILDFLAG(IS_MAC)
  gfx::Rect bounds = web_contents_->GetViewBounds();
  const gfx::NativeView web_contents_view =
      web_contents_->GetContentNativeView();
  views::Widget* widget =
      views::Widget::GetWidgetForNativeView(web_contents_view);
  ui::Layer* content_layer = widget->GetLayer();
  const gfx::Rect offset_bounds = widget->GetWindowBoundsInScreen();
  bounds.Offset(-offset_bounds.x(), -offset_bounds.y());

  event_capture_mac_ = std::make_unique<EventCaptureMac>(
      this,
      base::BindOnce(&ScreenshotFlow::CancelCapture, base::Unretained(this)),
      web_contents_view, web_contents_->GetTopLevelNativeWindow());
#else
  const gfx::NativeWindow& native_window = web_contents_->GetNativeView();
  ui::Layer* content_layer = native_window->layer();
  const gfx::Rect bounds = native_window->bounds();
  // Capture mouse down and drag events on our window.
  event_capture_.Observe(native_window);
#endif
  content_layer->Add(screen_capture_layer_.get());
  content_layer->StackAtTop(screen_capture_layer_.get());
  screen_capture_layer_->SetBounds(bounds);
  screen_capture_layer_->SetVisible(true);

  SetCursor(ui::mojom::CursorType::kCross);

  // After setup is done, we should set the capture mode to active.
  capture_mode_ = CaptureMode::SELECTION_RECTANGLE;
}

void ScreenshotFlow::RemoveUIOverlay() {
  if (capture_mode_ == CaptureMode::NOT_CAPTURING)
    return;
  is_dragging_ = false;
  capture_mode_ = CaptureMode::NOT_CAPTURING;
  if (!web_contents_ || !screen_capture_layer_)
    return;

#if BUILDFLAG(IS_MAC)
  views::Widget* widget = views::Widget::GetWidgetForNativeView(
      web_contents_->GetContentNativeView());
  if (!widget)
    return;
  ui::Layer* content_layer = widget->GetLayer();
  event_capture_mac_.reset();
#else
  const gfx::NativeWindow& native_window = web_contents_->GetNativeView();
#if BUILDFLAG(IS_WIN)
  // This handles cases where the overlay is removed while a drag is still in
  // progress, including if ScreenshotFlow is destroyed. It is safe to call
  // ReleaseCapture() even if the capture has not been set or has already been
  // released.
  native_window->ReleaseCapture();
#endif  // BUILDFLAG(IS_WIN)
  event_capture_.Reset();
  ui::Layer* content_layer = native_window->layer();
#endif  // else

  content_layer->Remove(screen_capture_layer_.get());

  screen_capture_layer_->set_delegate(nullptr);
  screen_capture_layer_.reset();

  // Restore the cursor to pointer; there's no corresponding GetCursor()
  // to store the pre-capture-mode cursor, and the pointer will have moved
  // in the meantime.
  SetCursor(ui::mojom::CursorType::kPointer);
}

void ScreenshotFlow::Start(ScreenshotCaptureCallback flow_callback) {
  flow_callback_ = std::move(flow_callback);
  CreateAndAddUIOverlay();
  RequestRepaint(gfx::Rect());
}

void ScreenshotFlow::StartFullscreenCapture(
    ScreenshotCaptureCallback flow_callback) {
  // Start and finish the capture process by screenshotting the full window.
  // There is no region selection step in this mode.
  flow_callback_ = std::move(flow_callback);
  CaptureAndRunScreenshotCompleteCallback(ScreenshotCaptureResultCode::SUCCESS,
                                          gfx::Rect(web_contents_->GetSize()));
}

void ScreenshotFlow::StartForRegionSelection(
    RegionSelectionCallback region_selection_callback) {
  region_selection_callback_ = std::move(region_selection_callback);
  CreateAndAddUIOverlay();
}

void ScreenshotFlow::CaptureAndRunScreenshotCompleteCallback(
    ScreenshotCaptureResultCode result_code,
    gfx::Rect region) {
  if (region.IsEmpty()) {
    RunScreenshotCompleteCallback(result_code, gfx::Rect(), gfx::Image());
    return;
  }

  gfx::Rect bounds = web_contents_->GetViewBounds();
  ui::GrabSnapshotImageCallback screenshot_callback =
      base::BindOnce(&ScreenshotFlow::RunScreenshotCompleteCallback, weak_this_,
                     result_code, bounds);
  ui::GrabViewSnapshot(web_contents_->GetNativeView(), region,
                       std::move(screenshot_callback));
}

void ScreenshotFlow::CancelCapture() {
  CompleteCapture(ScreenshotCaptureResultCode::USER_NAVIGATED_EXIT,
                  gfx::Rect());
}

void ScreenshotFlow::OnKeyEvent(ui::KeyEvent* event) {
  if (event->type() == ui::EventType::kKeyPressed &&
      event->key_code() == ui::VKEY_ESCAPE) {
    event->StopPropagation();
    CompleteCapture(ScreenshotCaptureResultCode::USER_ESCAPE_EXIT, gfx::Rect());
  }
}

void ScreenshotFlow::OnMouseEvent(ui::MouseEvent* event) {
  if (!event->IsLocatedEvent())
    return;
  const ui::LocatedEvent* located_event = ui::LocatedEvent::FromIfValid(event);
  if (!located_event)
    return;

  gfx::Point location = located_event->location();
  gfx::Rect web_contents_bounds = web_contents_->GetViewBounds();
#if BUILDFLAG(IS_MAC)
  // Offset |location| be relative to the WebContents widget, vs the parent
  // window, recomputed rather than cached in case e.g. user disables
  // bookmarks bar from another window.
  const gfx::NativeView web_contents_view =
      web_contents_->GetContentNativeView();
  views::Widget* widget =
      views::Widget::GetWidgetForNativeView(web_contents_view);
  if (!widget)
    return;
  const gfx::Rect widget_bounds = widget->GetWindowBoundsInScreen();
  location.set_x(location.x() + (widget_bounds.x() - web_contents_bounds.x()));
  location.set_y(location.y() + (widget_bounds.y() - web_contents_bounds.y()));
#endif
  switch (event->type()) {
    case ui::EventType::kMouseMoved:
      SetCursor(ui::mojom::CursorType::kCross);
      event->SetHandled();
      break;
    case ui::EventType::kMousePressed:
      if (event->IsOnlyLeftMouseButton()) {
        // Don't capture initial clicks on browser ui outside the webcontents.
        if (location.x() < 0 || location.y() < 0 ||
            location.x() > web_contents_bounds.width() ||
            location.y() > web_contents_bounds.height()) {
          return;
        }
        SetIsDragging(true);
        drag_start_ = location;
        drag_end_ = location;
        event->SetHandled();
      }
      break;
    case ui::EventType::kMouseDragged:
      if (event->IsOnlyLeftMouseButton() && is_dragging_) {
        drag_end_ = location;
        RequestRepaint(gfx::Rect());
        event->SetHandled();
      }
      break;
    case ui::EventType::kMouseReleased:
      if ((capture_mode_ == CaptureMode::SELECTION_RECTANGLE ||
           capture_mode_ == CaptureMode::SELECTION_ELEMENT) &&
          is_dragging_) {
        SetIsDragging(false);
        AttemptRegionCapture(web_contents_bounds);
        event->SetHandled();
      }
      break;
    // This event type is never called on Mac.
    case ui::EventType::kMousewheel:
      if ((capture_mode_ == CaptureMode::SELECTION_RECTANGLE ||
           capture_mode_ == CaptureMode::SELECTION_ELEMENT) &&
          event->AsMouseWheelEvent()->y_offset() > 0 && is_dragging_) {
        SetIsDragging(false);
        AttemptRegionCapture(web_contents_bounds);
        event->SetHandled();
      }
      break;
    default:
      break;
  }
}

void ScreenshotFlow::OnScrollEvent(ui::ScrollEvent* event) {
  // A single tap can create a scroll event, so ignore scroll starts and
  // cancels but complete capture when scrolls actually occur.
  if (event->type() == ui::EventType::kScrollFlingStart ||
      event->type() == ui::EventType::kScrollFlingCancel) {
    return;
  }

  gfx::Rect web_contents_bounds = web_contents_->GetViewBounds();
  if ((capture_mode_ == CaptureMode::SELECTION_RECTANGLE ||
       capture_mode_ == CaptureMode::SELECTION_ELEMENT) &&
      event->y_offset() > 0 && is_dragging_) {
    SetIsDragging(false);
    AttemptRegionCapture(web_contents_bounds);
    event->SetHandled();
  }
}

void ScreenshotFlow::CompleteCapture(ScreenshotCaptureResultCode result_code,
                                     const gfx::Rect& region) {
  RemoveUIOverlay();
  if (region_selection_callback_) {
    RegionSelectionResult result = {.result_code = result_code,
                                    .selected_rect = region};
    std::move(region_selection_callback_).Run(result);
    return;
  }
  CaptureAndRunScreenshotCompleteCallback(result_code, region);
}

void ScreenshotFlow::RunScreenshotCompleteCallback(
    ScreenshotCaptureResultCode result_code,
    gfx::Rect bounds,
    gfx::Image image) {
  if (flow_callback_.is_null())
    return;

  ScreenshotCaptureResult result;
  result.result_code = result_code;
  result.image = image;
  result.screen_bounds = bounds;

  std::move(flow_callback_).Run(result);
}

bool ScreenshotFlow::IsUIOverlayShown() {
  return screen_capture_layer_ != nullptr && screen_capture_layer_->visible();
}

void ScreenshotFlow::ResetUIOverlayBounds() {
#if BUILDFLAG(IS_MAC)
  gfx::Rect bounds = web_contents_->GetViewBounds();
  const gfx::NativeView web_contents_view =
      web_contents_->GetContentNativeView();
  views::Widget* widget =
      views::Widget::GetWidgetForNativeView(web_contents_view);
  if (widget != nullptr) {
    const gfx::Rect offset_bounds = widget->GetWindowBoundsInScreen();
    bounds.Offset(-offset_bounds.x(), -offset_bounds.y());
  }
#else
  const gfx::NativeWindow& native_window = web_contents_->GetNativeView();
  const gfx::Rect bounds = native_window->bounds();
#endif
  screen_capture_layer_->SetBounds(bounds);
}

void ScreenshotFlow::OnPaintLayer(const ui::PaintContext& context) {
  if (!screen_capture_layer_)
    return;

  const gfx::Rect& screen_bounds(screen_capture_layer_->bounds());
  ui::PaintRecorder recorder(context, screen_bounds.size());
  gfx::Canvas* canvas = recorder.canvas();

  auto selection_rect = gfx::BoundingRect(drag_start_, drag_end_);
  // Draw border exclusively outside selected region.
  selection_rect.Inset(gfx::Insets::TLBR(-1, -1, 0, 0));
  PaintSelectionLayer(canvas, selection_rect, gfx::Rect());
  paint_invalidation_ = gfx::Rect();
}

void ScreenshotFlow::RequestRepaint(gfx::Rect region) {
  if (!screen_capture_layer_)
    return;

  if (region.IsEmpty()) {
    const gfx::Size& layer_size = screen_capture_layer_->size();
    region = gfx::Rect(0, 0, layer_size.width(), layer_size.height());
  }

  paint_invalidation_.Union(region);
  screen_capture_layer_->SchedulePaint(region);
}

void ScreenshotFlow::PaintSelectionLayer(gfx::Canvas* canvas,
                                         const gfx::Rect& selection,
                                         const gfx::Rect& invalidation_region) {
  // Adjust for hidpi and lodpi support.
  canvas->UndoDeviceScaleFactor();

  // Clear the canvas with our mask color.
  canvas->DrawColor(kColorSemitransparentOverlayMask);
  // Allow the user's selection to show through, and add a border around it.
  if (!selection.IsEmpty()) {
    float scale_factor = screen_capture_layer_->device_scale_factor();
    gfx::Rect selection_scaled =
        gfx::ScaleToEnclosingRect(selection, scale_factor);
    canvas->FillRect(selection_scaled, kColorSemitransparentOverlayVisible,
                     SkBlendMode::kClear);
    canvas->DrawRect(gfx::RectF(selection_scaled), kColorSelectionRect);
  }
}

void ScreenshotFlow::SetCursor(ui::mojom::CursorType cursor_type) {
  if (!web_contents_) {
    return;
  }

#if BUILDFLAG(IS_MAC)
  if (cursor_type == ui::mojom::CursorType::kCross) {
    EventCaptureMac::SetCrossCursor();
    return;
  }
#endif

  content::RenderWidgetHost* host =
      web_contents_->GetPrimaryMainFrame()->GetRenderWidgetHost();
  if (host) {
    ui::Cursor cursor(cursor_type);
    host->SetCursor(cursor);
  }
}

void ScreenshotFlow::SetIsDragging(bool value) {
  is_dragging_ = value;
#if BUILDFLAG(IS_WIN)
  const gfx::NativeWindow& native_window = web_contents_->GetNativeView();
  if (value) {
    native_window->SetCapture();
  } else {
    native_window->ReleaseCapture();
  }
#endif
}

bool ScreenshotFlow::IsCaptureModeActive() {
  return capture_mode_ != CaptureMode::NOT_CAPTURING;
}

void ScreenshotFlow::WebContentsDestroyed() {
  if (IsCaptureModeActive()) {
    CancelCapture();
  }
}

void ScreenshotFlow::OnVisibilityChanged(content::Visibility visibility) {
  if (IsCaptureModeActive()) {
    CancelCapture();
  }
}

void ScreenshotFlow::AttemptRegionCapture(gfx::Rect view_bounds) {
  // On Mac, it is possible for drag_end_ to end up outside of the
  // Browser UI. This causes an error when completing the region
  // capture. Update drag_end_ to be within the web content bounds.
  drag_end_.SetToMin(gfx::Point(view_bounds.width(), view_bounds.height()));
  drag_end_.SetToMax(gfx::Point(0, 0));

  gfx::Rect selection = gfx::BoundingRect(drag_start_, drag_end_);
  drag_start_.SetPoint(0, 0);
  drag_end_.SetPoint(0, 0);
  if (selection.width() >= kMinimumValidSelectionEdgePixels &&
      selection.height() >= kMinimumValidSelectionEdgePixels) {
    CompleteCapture(ScreenshotCaptureResultCode::SUCCESS, selection);
  } else {
    RequestRepaint(gfx::Rect());
  }
}

}  // namespace image_editor
