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

#include <algorithm>

#include "base/memory/raw_ptr.h"
#include "build/build_config.h"
#include "components/eye_dropper/eye_dropper_view.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/client/focus_change_observer.h"
#include "ui/aura/client/focus_client.h"
#include "ui/aura/window.h"
#include "ui/base/ui_base_features.h"
#include "ui/display/screen.h"
#include "ui/events/event.h"
#include "ui/native_window_tracker/native_window_tracker.h"
#include "ui/views/widget/widget.h"

namespace eye_dropper {
namespace {
gfx::Point ClampToDisplay(const gfx::Point& point) {
  gfx::Rect bounds =
      display::Screen::Get()->GetDisplayNearestPoint(point).bounds();
  return gfx::Point(std::clamp(point.x(), bounds.x(), bounds.right() - 1),
                    std::clamp(point.y(), bounds.y(), bounds.bottom() - 1));
}
}  // namespace

class EyeDropperView::PreEventDispatchHandler::KeyboardHandler
    : public ui::EventHandler {
 public:
  KeyboardHandler(EyeDropperView* view, aura::Window* parent);
  KeyboardHandler(const KeyboardHandler&) = delete;
  KeyboardHandler& operator=(const KeyboardHandler&) = delete;
  ~KeyboardHandler() override;

 private:
  void OnKeyEvent(ui::KeyEvent* event) override;

  raw_ptr<EyeDropperView> view_;
  raw_ptr<aura::Window> parent_;
  std::unique_ptr<ui::NativeWindowTracker> parent_tracker_;
};

EyeDropperView::PreEventDispatchHandler::KeyboardHandler::KeyboardHandler(
    EyeDropperView* view,
    aura::Window* parent)
    : view_(view),
      parent_(parent),
      parent_tracker_(ui::NativeWindowTracker::Create(parent)) {
  // Because the eye dropper is not focused in order to not dismiss the color
  // popup, we need to listen for key events on the parent window that has
  // focus.
  parent_->AddPreTargetHandler(this, ui::EventTarget::Priority::kSystem);
}

EyeDropperView::PreEventDispatchHandler::KeyboardHandler::~KeyboardHandler() {
  if (!parent_tracker_->WasNativeWindowDestroyed()) {
    parent_->RemovePreTargetHandler(this);
  }
}

void EyeDropperView::PreEventDispatchHandler::KeyboardHandler::OnKeyEvent(
    ui::KeyEvent* event) {
  if (event->type() != ui::EventType::kKeyPressed) {
    return;
  }

  // Move when arrow keys are pressed, move faster if shift is down.
  const auto move_by = [&](int x, int y) {
    if (event->IsShiftDown()) {
      x *= 10;
      y *= 10;
    }
    const auto center =
        view_->GetWidget()->GetWindowBoundsInScreen().CenterPoint();
    view_->UpdatePosition(ClampToDisplay(center + gfx::Vector2d(x, y)));
  };

  switch (event->key_code()) {
    case ui::VKEY_ESCAPE:
      view_->OnColorSelectionCanceled();
      break;
    case ui::VKEY_RETURN:
    case ui::VKEY_SPACE:
      view_->OnColorSelected();
      break;
    case ui::VKEY_UP:
      move_by(0, -1);
      break;
    case ui::VKEY_DOWN:
      move_by(0, 1);
      break;
    case ui::VKEY_LEFT:
      move_by(-1, 0);
      break;
    case ui::VKEY_RIGHT:
      move_by(1, 0);
      break;
    default:
      return;
  }
  event->StopPropagation();
}

class EyeDropperView::PreEventDispatchHandler::FocusObserver
    : public aura::client::FocusChangeObserver {
 public:
  FocusObserver(EyeDropperView* view, aura::Window* parent);

  // aura::client::FocusChangeObserver
  void OnWindowFocused(aura::Window* gained_focus,
                       aura::Window* lost_focus) override;

  raw_ptr<EyeDropperView> view_;
  base::ScopedObservation<aura::client::FocusClient,
                          aura::client::FocusChangeObserver>
      focus_observation_{this};
};

EyeDropperView::PreEventDispatchHandler::FocusObserver::FocusObserver(
    EyeDropperView* view,
    aura::Window* parent)
    : view_(view) {
  focus_observation_.Observe(
      aura::client::GetFocusClient(parent->GetRootWindow()));
}

void EyeDropperView::PreEventDispatchHandler::FocusObserver::OnWindowFocused(
    aura::Window* gained_focus,
    aura::Window* lost_focus) {
  view_->OnColorSelectionCanceled();
}

EyeDropperView::PreEventDispatchHandler::PreEventDispatchHandler(
    EyeDropperView* view,
    gfx::NativeView parent)
    : view_(view),
      keyboard_handler_(std::make_unique<KeyboardHandler>(view, parent)),
      focus_observer_(std::make_unique<FocusObserver>(view, parent)) {
  // Ensure that this handler is called before color popup handler by using
  // a higher priority.
  view->GetWidget()->GetNativeWindow()->AddPreTargetHandler(
      this, ui::EventTarget::Priority::kSystem);
}

EyeDropperView::PreEventDispatchHandler::~PreEventDispatchHandler() {
  view_->GetWidget()->GetNativeWindow()->RemovePreTargetHandler(this);
}

void EyeDropperView::PreEventDispatchHandler::OnMouseEvent(
    ui::MouseEvent* event) {
  if (event->type() == ui::EventType::kMousePressed) {
    // Avoid closing the color popup when the eye dropper is clicked.
    event->StopPropagation();
  }
  if (event->type() == ui::EventType::kMouseReleased) {
    view_->OnColorSelected();
    event->StopPropagation();
  }
}

void EyeDropperView::PreEventDispatchHandler::OnGestureEvent(
    ui::GestureEvent* event) {
  if (event->type() == ui::EventType::kGestureTap) {
    view_->OnColorSelected();
    event->StopPropagation();
  }
}

void EyeDropperView::PreEventDispatchHandler::OnTouchEvent(
    ui::TouchEvent* event) {
  if (event->type() == ui::EventType::kTouchPressed) {
    // For touch-move, we don't move the center of the EyeDropper to be at the
    // touch point, but rather maintain the offset from the first press.
    touch_offset_ = event->root_location() -
                    view_->GetWidget()->GetWindowBoundsInScreen().CenterPoint();
  }
  if (event->type() == ui::EventType::kTouchMoved) {
    // Keep EyeDropper always inside a display, but adjust offset when position
    // is clamped.
    gfx::Point position = event->root_location() - touch_offset_;
    gfx::Point clamped = ClampToDisplay(position);
    if (clamped != position) {
      touch_offset_ = event->root_location() - clamped;
    }
    view_->UpdatePosition(std::move(clamped));
  }
}

void EyeDropperView::CaptureInput() {
  // The eye dropper needs to capture input since it is not activated
  // in order to avoid dismissing the color picker.
  GetWidget()->GetNativeWindow()->SetCapture();
}

void EyeDropperView::HideCursor() {
  auto* cursor_client = aura::client::GetCursorClient(
      GetWidget()->GetNativeWindow()->GetRootWindow());
  cursor_client->HideCursor();
  cursor_client->LockCursor();
}

void EyeDropperView::ShowCursor() {
  aura::client::GetCursorClient(GetWidget()->GetNativeWindow()->GetRootWindow())
      ->UnlockCursor();
}
}  // namespace eye_dropper
