// Copyright 2026 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/glic/glic_selection_observer.h"

#include "base/containers/flat_set.h"
#include "base/containers/span.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/string_tokenizer.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "build/build_config.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/enterprise/data_protection/data_protection_clipboard_utils.h"
#include "chrome/browser/feature_engagement/tracker_factory.h"
#include "chrome/browser/glic/browser_ui/glic_nudge_controller.h"
#include "chrome/browser/glic/browser_ui/glic_selection_widget.h"
#include "chrome/browser/glic/common/local_hotkey_manager.h"
#include "chrome/browser/glic/glic_pref_names.h"
#include "chrome/browser/glic/glic_zero_state_suggestions_manager.h"
#include "chrome/browser/glic/host/context/glic_sharing_utils.h"
#include "chrome/browser/glic/host/glic.mojom.h"
#include "chrome/browser/glic/public/features.h"
#include "chrome/browser/glic/public/glic_enabling.h"
#include "chrome/browser/glic/public/glic_instance.h"
#include "chrome/browser/glic/public/glic_invoke_options.h"
#include "chrome/browser/glic/public/glic_keyed_service.h"
#include "chrome/browser/glic/public/glic_passkeys.h"
#include "chrome/browser/glic/public/service/glic_instance_coordinator.h"
#include "chrome/browser/glic/selection/explain_selection_trigger.h"
#include "chrome/browser/glic/selection/inline_cue_blocklist_utils.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/skills/skills_service_factory.h"
#include "chrome/browser/skills/skills_update_observer.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/chrome_pages.h"
#include "chrome/browser/ui/tabs/page_context_eligibility_helper.h"
#include "chrome/browser/ui/toasts/api/toast_id.h"
#include "chrome/browser/ui/toasts/toast_controller.h"
#include "chrome/browser/ui/toasts/toast_features.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings_pattern.h"
#include "components/feature_engagement/public/tracker.h"
#include "components/optimization_guide/content/browser/page_context_eligibility.h"
#include "components/optimization_guide/content/browser/page_context_eligibility_observer.h"
#include "components/prefs/pref_service.h"
#include "components/shared_highlighting/core/common/disabled_sites.h"
#include "components/shared_highlighting/core/common/fragment_directives_utils.h"
#include "components/shared_highlighting/core/common/shared_highlighting_features.h"
#include "components/signin/public/identity_manager/account_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/skills/features.h"
#include "components/skills/public/skills_service.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/clipboard_types.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_utils.h"
#include "mojo/public/cpp/base/big_buffer.h"
#include "services/service_manager/public/cpp/interface_provider.h"
#include "third_party/blink/public/common/input/web_input_event.h"
#include "third_party/blink/public/common/input/web_keyboard_event.h"
#include "third_party/blink/public/common/input/web_mouse_event.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/text_elider.h"
#include "ui/views/widget/widget.h"

namespace glic {

namespace {

// The maximum length of the selection text sent as a suggested prompt.
// Selections longer than this are ignored.
// We send a truncated version of the selection to the panel as it does not
// need to be the full text.
constexpr size_t kMaxSelectionLength = 1000;
constexpr size_t kMaxSelectionLengthSentToPanel = 100;

// The minimum length of the selection text sent as a suggested prompt.
// Selections shorter than this are ignored.
constexpr size_t kMinSelectionLength = 3;

// The MIME type for selected text.
constexpr char kSelectionMimeType[] = "application/x-glic-selection";

// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// LINT.IfChange(GlicSelectionAction)
enum class GlicSelectionAction {
  kNudgeShown = 0,
  kWidgetShown = 1,
  kNudgeClicked = 2,
  kWidgetClicked = 3,
  kWidgetDismissedByButton = 4,
  kWidgetDismissedByClickOutside = 5,
  kMaxValue = kWidgetDismissedByClickOutside
};
// LINT.ThenChange(//tools/metrics/histograms/metadata/glic/enums.xml:GlicSelectionAction)

size_t CountWords(std::u16string_view text) {
  size_t count = 0;
  base::StringView16Tokenizer tokenizer(
      text, u"", base::StringView16Tokenizer::WhitespacePolicy::kSkipOver);
  while (tokenizer.GetNext()) {
    ++count;
  }
  return count;
}

mojom::AdditionalContextPtr CreateAdditionalContext(
    content::WebContents* web_contents,
    const std::u16string& selected_text) {
  auto context = mojom::AdditionalContext::New();
  context->source = mojom::AdditionalContextSource::kTextSelection;
  std::vector<mojom::AdditionalContextPartPtr> parts;
  if (!selected_text.empty()) {
    auto context_data = mojom::ContextData::New();
    context_data->mime_type = kSelectionMimeType;
    std::u16string elided_text;
    gfx::ElideString(selected_text, kMaxSelectionLengthSentToPanel,
                     &elided_text);
    std::string utf8_text = base::UTF16ToUTF8(elided_text);
    context_data->data =
        mojo_base::BigBuffer(base::as_bytes(base::span(utf8_text)));
    parts.push_back(
        mojom::AdditionalContextPart::NewData(std::move(context_data)));
  }
  if (auto* tab_interface =
          tabs::TabInterface::MaybeGetFromContents(web_contents)) {
    context->tab_id = tab_interface->GetHandle().raw_value();
  }
  context->parts = std::move(parts);
  return context;
}

// Minimum distance in pixels required for a mouse move step to establish or
// change movement direction.
constexpr float kMinShakeDistance = 10.0f;
// Required number of direction changes to trigger region capture.
constexpr int kRequiredDirectionChanges = 4;
// Maximum time allowed between direction changes before the shake detector resets.
constexpr base::TimeDelta kShakeTimeout = base::Milliseconds(1000);

bool IsListenedToInputEvent(blink::WebInputEvent::Type type) {
  switch (type) {
    case blink::WebInputEvent::Type::kMouseDown:
    case blink::WebInputEvent::Type::kPointerDown:
    case blink::WebInputEvent::Type::kGestureTapDown:
    case blink::WebInputEvent::Type::kTouchStart:
    case blink::WebInputEvent::Type::kMouseUp:
    case blink::WebInputEvent::Type::kPointerUp:
    case blink::WebInputEvent::Type::kPointerCancel:
    case blink::WebInputEvent::Type::kTouchEnd:
    case blink::WebInputEvent::Type::kTouchCancel:
    case blink::WebInputEvent::Type::kGestureTapCancel:
    case blink::WebInputEvent::Type::kKeyUp:
    case blink::WebInputEvent::Type::kRawKeyDown:
    case blink::WebInputEvent::Type::kKeyDown:
    case blink::WebInputEvent::Type::kGestureScrollBegin:
    case blink::WebInputEvent::Type::kMouseWheel:
    case blink::WebInputEvent::Type::kMouseMove:
      return true;
    default:
      return false;
  }
}

}  // namespace

class GlicSelectionObserver::WidgetActionDelegate
    : public GlicSelectionWidgetDelegate::ActionDelegate {
 public:
  explicit WidgetActionDelegate(GlicSelectionObserver* observer)
      : observer_(observer) {}

  // GlicSelectionWidgetDelegate::ActionDelegate:
  void OnAskGemini() override { observer_->OnAskGemini(); }
  void OnAskGeminiWithSkill(
      const GlicSelectionWidgetDelegate::SkillOption& skill) override {
    observer_->OnAskGeminiWithSkill(skill);
  }
  void OnAskGeminiForQuery(const std::u16string& query) override {
    observer_->OnAskGeminiForQuery(query);
  }
  void OnAskGeminiMoreAboutThis(
      const std::u16string& selected_text,
      const std::string& explanation_text) override {
    observer_->OnAskGeminiMoreAboutThis(selected_text, explanation_text);
  }
  void OnCopy() override { observer_->OnCopy(); }
  void OnCopyLink() override { observer_->OnCopyLink(); }
  void OnHide() override { observer_->OnHide(); }
  void OnSettings() override { observer_->OnSettings(); }
  void OnOpenInSidePanel() override { observer_->OnOpenInSidePanel(); }
  void OnWidgetClose() override { observer_->OnWidgetClose(); }
  bool IsInlineFulfillmentSupported() override {
    return ExplainSelectionTrigger::IsInlineFulfillmentSupported();
  }
  std::vector<GlicSelectionWidgetDelegate::SkillOption> GetContextualSkills()
      override {
    return observer_->GetContextualSkills();
  }
  std::vector<GlicSelectionWidgetDelegate::SkillOption> GetUserSkills()
      override {
    return observer_->GetUserSkills();
  }

 private:
  raw_ptr<GlicSelectionObserver> observer_;
};

GlicSelectionObserver::GlicSelectionObserver(content::WebContents* web_contents)
    : content::WebContentsObserver(web_contents),
      action_delegate_(std::make_unique<WidgetActionDelegate>(this)) {
  CHECK(web_contents);
  Profile* profile =
      Profile::FromBrowserContext(web_contents->GetBrowserContext());
  glic_keyed_service_ = GlicKeyedService::Get(profile);

  if (glic_keyed_service_) {
    panel_state_subscription_ =
        glic_keyed_service_->instance_coordinator().AddGlobalShowHideCallback(
            base::BindRepeating(&GlicSelectionObserver::OnGlobalPanelShowHide,
                                weak_ptr_factory_.GetWeakPtr()));
  }

  std::string account;
  if (profile) {
    auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
    if (identity_manager) {
      account =
          identity_manager->GetPrimaryAccountInfo(signin::ConsentLevel::kSignin)
              .email;
    }
  }

  auto* tab_interface = tabs::TabInterface::MaybeGetFromContents(web_contents);
  auto* helper = tab_interface
                     ? tabs::PageContextEligibilityHelper::From(tab_interface)
                     : nullptr;
  if (helper) {
    page_context_eligibility_subscription_ =
        helper->RegisterEligibilityChangeCallback(base::BindRepeating(
            &GlicSelectionObserver::OnPageContextEligibilityChanged,
            weak_ptr_factory_.GetWeakPtr()));
  } else {
    CreatePageContextEligibilityAPI(std::move(account));
  }

  if (profile) {
    if (auto* settings_map =
            HostContentSettingsMapFactory::GetForProfile(profile)) {
      content_settings_observation_.Observe(settings_map);
    }
  }
  UpdatePageBlockedState();

  web_contents->ForEachRenderFrameHost(
      [this](content::RenderFrameHost* render_frame_host) {
        RenderFrameCreated(render_frame_host);
      });
  explain_selection_trigger_ = std::make_unique<ExplainSelectionTrigger>();
}

bool GlicSelectionObserver::IsSelectionPromptEnabled() const {
  Profile* profile =
      Profile::FromBrowserContext(web_contents()->GetBrowserContext());
  auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
  if (!identity_manager ||
      !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
    return false;
  }
  return GlicEnabling::IsSelectionPromptEnabledForProfile(profile);
}

GlicSelectionObserver::~GlicSelectionObserver() {
  widget_delegate_.reset();

  base::flat_set<content::RenderWidgetHost*> unique_rwhs;
  for (const auto& frame_token : observed_frames_) {
    content::RenderFrameHost* rfh =
        content::RenderFrameHost::FromFrameToken(frame_token);
    if (rfh && rfh->GetRenderWidgetHost()) {
      unique_rwhs.insert(rfh->GetRenderWidgetHost());
    }
  }
  for (auto* rwh : unique_rwhs) {
    rwh->RemoveInputEventObserver(this);
  }
  observed_frames_.clear();
}

void GlicSelectionObserver::RenderFrameCreated(
    content::RenderFrameHost* render_frame_host) {
  if (auto* rwh = render_frame_host->GetRenderWidgetHost()) {
    bool already_observing = false;
    for (const auto& frame_token : observed_frames_) {
      content::RenderFrameHost* rfh =
          content::RenderFrameHost::FromFrameToken(frame_token);
      if (rfh && rfh->GetRenderWidgetHost() == rwh) {
        already_observing = true;
        break;
      }
    }
    if (observed_frames_.insert(render_frame_host->GetGlobalFrameToken())
            .second) {
      if (!already_observing) {
        rwh->AddInputEventObserver(this);
      }
    }
  }
}

void GlicSelectionObserver::RenderFrameDeleted(
    content::RenderFrameHost* render_frame_host) {
  if (!observed_frames_.contains(render_frame_host->GetGlobalFrameToken())) {
    return;
  }

  content::RenderWidgetHost* rwh = render_frame_host->GetRenderWidgetHost();
  observed_frames_.erase(render_frame_host->GetGlobalFrameToken());

  bool still_observing = false;
  for (const auto& frame_token : observed_frames_) {
    content::RenderFrameHost* rfh =
        content::RenderFrameHost::FromFrameToken(frame_token);
    if (rfh && rfh->GetRenderWidgetHost() == rwh) {
      still_observing = true;
      break;
    }
  }
  if (!still_observing && rwh) {
    rwh->RemoveInputEventObserver(this);
  }
}
void GlicSelectionObserver::OnVisibilityChanged(
    content::Visibility visibility) {
  if (visibility == content::Visibility::HIDDEN && widget_delegate_) {
    is_explaining_ = false;
    widget_delegate_->CloseWidget();
  }
}

void GlicSelectionObserver::PrimaryPageChanged(content::Page& page) {
  is_hidden_on_current_page_ = false;
  UpdatePageBlockedState();
  if (widget_delegate_) {
    is_explaining_ = false;
    widget_delegate_->CloseWidget();
  }
}

void GlicSelectionObserver::PrimaryMainFrameWasResized(bool width_changed) {
  DismissUI(DismissReason::kExternal);
}

void GlicSelectionObserver::OnWebContentsLostFocus(
    content::RenderWidgetHost* render_widget_host) {
  if (web_contents()->IsBeingDestroyed()) {
    ResetPendingSelection();
    return;
  }

  // If the web contents loses focus, process any pending selection immediately.
  ProcessPendingSelection();
}

void GlicSelectionObserver::OnInputEvent(
    const content::RenderWidgetHost& host,
    const blink::WebInputEvent& event,
    content::RenderWidgetHost::InputEventObserver::InputEventSource source) {
  if (!IsListenedToInputEvent(event.GetType())) {
    return;
  }
  if (!IsTabValidForSharing(web_contents())) {
    return;
  }
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(&GlicSelectionObserver::ProcessInputEvent,
                                weak_ptr_factory_.GetWeakPtr(), event.Clone()));
}

void GlicSelectionObserver::ProcessInputEvent(
    std::unique_ptr<blink::WebInputEvent> event) {
  if (!IsSelectionPromptEnabled()) {
    return;
  }

  switch (event->GetType()) {
    case blink::WebInputEvent::Type::kMouseMove: {
      const auto& mouse_event =
          static_cast<const blink::WebMouseEvent&>(*event);
      ProcessMouseMoveForShake(mouse_event);
      break;
    }

    case blink::WebInputEvent::Type::kMouseDown:
    case blink::WebInputEvent::Type::kPointerDown:
    case blink::WebInputEvent::Type::kGestureTapDown:
    case blink::WebInputEvent::Type::kTouchStart: {
      ResetShakeDetector();
      bool is_left_click_or_touch = true;
      if (event->GetType() == blink::WebInputEvent::Type::kMouseDown ||
          event->GetType() == blink::WebInputEvent::Type::kPointerDown) {
        const auto& mouse_event =
            static_cast<const blink::WebMouseEvent&>(*event);
        if (mouse_event.button != blink::WebPointerProperties::Button::kLeft) {
          is_left_click_or_touch = false;
        }
      }

      is_key_selection_ = false;
      bounds_retry_count_ = 0;
      DismissUI(DismissReason::kExternal);

      // Workaround for a bug in Blink: when a user single-clicks directly on
      // top of an existing selection, Blink collapses the selection on MouseUp
      // but fails to send the corresponding OnTextSelectionChanged(empty) IPC.
      // Since any left-click or touch tap invalidates the current static text
      // selection (by either placing the caret, clearing the selection, or
      // initiating a new drag), we preemptively clear the context here to
      // ensure it is not left hanging.
      if (is_left_click_or_touch) {
        is_selecting_ = true;
        ResetPendingSelection();
        if (has_sent_selection_context_) {
          UpdateSelectionState(std::u16string(),
                               /*is_pending_selection=*/false);
        }
      }
      break;
    }

    case blink::WebInputEvent::Type::kMouseUp:
    case blink::WebInputEvent::Type::kPointerUp:
    case blink::WebInputEvent::Type::kPointerCancel:
    case blink::WebInputEvent::Type::kTouchEnd:
    case blink::WebInputEvent::Type::kTouchCancel:
    case blink::WebInputEvent::Type::kGestureTapCancel:
      ResetShakeDetector();
      // Process the selection received so far. If the final selection IPC is
      // delayed, OnTextSelectionChanged will handle it since `is_selecting_`
      // becomes false.
      ProcessPendingSelection();
      break;

    case blink::WebInputEvent::Type::kKeyUp:
      if (is_key_selection_) {
        ProcessPendingSelection();
      }
      break;

    case blink::WebInputEvent::Type::kRawKeyDown:
    case blink::WebInputEvent::Type::kKeyDown: {
      ResetShakeDetector();
      if (is_key_selection_) {
        break;
      }
      DismissUI(DismissReason::kExternal);
      const auto& keyboard_event =
          static_cast<const blink::WebKeyboardEvent&>(*event);
#if BUILDFLAG(IS_MAC)
      int select_all_modifier = blink::WebInputEvent::Modifiers::kMetaKey;
#else
      int select_all_modifier = blink::WebInputEvent::Modifiers::kControlKey;
#endif
      bool is_select_all = (event->GetModifiers() & select_all_modifier) &&
                           keyboard_event.windows_key_code == ui::VKEY_A;
      bool is_shift =
          event->GetModifiers() & blink::WebInputEvent::Modifiers::kShiftKey;
      bool is_navigation_key =
          keyboard_event.windows_key_code == ui::VKEY_LEFT ||
          keyboard_event.windows_key_code == ui::VKEY_RIGHT ||
          keyboard_event.windows_key_code == ui::VKEY_UP ||
          keyboard_event.windows_key_code == ui::VKEY_DOWN ||
          keyboard_event.windows_key_code == ui::VKEY_HOME ||
          keyboard_event.windows_key_code == ui::VKEY_END ||
          keyboard_event.windows_key_code == ui::VKEY_PRIOR ||
          keyboard_event.windows_key_code == ui::VKEY_NEXT;
      if ((is_shift && is_navigation_key) || is_select_all) {
        is_key_selection_ = true;
        is_selecting_ = true;
        if (!last_selected_text_.empty()) {
          pending_selection_text_ = last_selected_text_;
        } else {
          ResetPendingSelection();
        }
      }
      break;
    }

    case blink::WebInputEvent::Type::kGestureScrollBegin:
    case blink::WebInputEvent::Type::kMouseWheel:
      ResetShakeDetector();
      DismissUI(DismissReason::kExternal);
      break;

    default:
      break;
  }
}

void GlicSelectionObserver::OnTextSelectionChanged(
    content::RenderFrameHost* render_frame_host,
    std::u16string_view selected_text) {
  if (!IsSelectionPromptEnabled()) {
    return;
  }

  if (!IsTabValidForSharing(web_contents())) {
    return;
  }

  if (web_contents()->IsFocusedElementEditable()) {
    selected_text = std::u16string_view();
  }

  bounds_retry_count_ = 0;

  std::u16string_view trimmed_text =
      base::TrimWhitespace(selected_text, base::TRIM_ALL);

  if (trimmed_text.length() > kMaxSelectionLength) {
    pending_selection_text_ = std::u16string();
  } else {
    size_t non_whitespace_count = 0;
    bool exceeds_minimum_selection_length = false;
    for (char16_t c : trimmed_text) {
      if (!base::IsUnicodeWhitespace(c)) {
        non_whitespace_count++;
      }
      if (non_whitespace_count == kMinSelectionLength) {
        exceeds_minimum_selection_length = true;
        break;
      }
    }

    if (!exceeds_minimum_selection_length) {
      pending_selection_text_ = std::u16string();
    } else {
      pending_selection_text_ = std::u16string(trimmed_text);
    }
  }
  if (render_frame_host) {
    last_selection_frame_token_ = render_frame_host->GetGlobalFrameToken();
  }

  // If not in the process of selecting, process the selection immediately.
  if (!is_selecting_) {
    ProcessPendingSelection();
  }
}

void GlicSelectionObserver::DismissUI(DismissReason reason) {
  if (widget_delegate_ && !is_explaining_) {
    if (!dismissal_recorded_ && reason != DismissReason::kActionTaken) {
      bool is_post_fre = false;
      if (web_contents()) {
        Profile* profile =
            Profile::FromBrowserContext(web_contents()->GetBrowserContext());
        is_post_fre = GlicEnabling::HasConsentedForProfile(profile);
      }
      const char* histogram_suffix = is_post_fre ? ".PostFre" : ".PreFre";
      GlicSelectionAction action =
          (reason == DismissReason::kCloseButton)
              ? GlicSelectionAction::kWidgetDismissedByButton
              : GlicSelectionAction::kWidgetDismissedByClickOutside;
      base::UmaHistogramEnumeration(
          base::StrCat({"Glic.Selection.Action", histogram_suffix}), action);
    }
    dismissal_recorded_ = true;
    widget_delegate_->CloseWidget();
  }
}

void GlicSelectionObserver::ProcessPendingSelection() {
  is_selecting_ = false;
  is_key_selection_ = false;
  if (!pending_selection_text_.has_value()) {
    return;
  }

  std::u16string selected_text = std::move(*pending_selection_text_);
  ResetPendingSelection();

  UpdateSelectionState(selected_text, /*is_pending_selection=*/true);
}

void GlicSelectionObserver::ResetPendingSelection() {
  pending_selection_text_.reset();
}

// static
void GlicSelectionObserver::InvokeGlicFromSelectionAffordance(
    std::u16string selected_text,
    bool is_widget,
    base::WeakPtr<content::WebContents> web_contents,
    GlicNudgeActivity activity,
    std::u16string prompt_override,
    const GlicSelectionWidgetDelegate::SkillOption& skill,
    const std::string& skill_prompt) {
  if (activity != GlicNudgeActivity::kNudgeClicked) {
    return;
  }

  bool is_post_fre = false;
  if (web_contents) {
    Profile* profile =
        Profile::FromBrowserContext(web_contents->GetBrowserContext());
    is_post_fre = GlicEnabling::HasConsentedForProfile(profile);
  }

  const char* histogram_suffix = is_post_fre ? ".PostFre" : ".PreFre";

  base::UmaHistogramEnumeration(
      base::StrCat({"Glic.Selection.Action", histogram_suffix}),
      is_widget ? GlicSelectionAction::kWidgetClicked
                : GlicSelectionAction::kNudgeClicked);
  if (is_widget) {
    base::UmaHistogramCounts1000(
        base::StrCat(
            {"Glic.Selection.WidgetClicked.SelectionLength", histogram_suffix}),
        selected_text.length());
    base::UmaHistogramCounts1000(
        base::StrCat({"Glic.Selection.WidgetClicked.SelectionWordCount",
                      histogram_suffix}),
        CountWords(selected_text));
  } else {
    base::UmaHistogramCounts1000(
        base::StrCat(
            {"Glic.Selection.NudgeClicked.SelectionLength", histogram_suffix}),
        selected_text.length());
  }

  if (web_contents) {
    if (auto* tab_interface =
            tabs::TabInterface::MaybeGetFromContents(web_contents.get())) {
      if (tab_interface->GetBrowserWindowInterface()) {
        Profile* profile =
            Profile::FromBrowserContext(web_contents->GetBrowserContext());
        if (auto* glic_keyed_service = GlicKeyedService::Get(profile)) {
          GlicInvokeOptions options(glic::Target(*tab_interface),
                                    mojom::InvocationSource::kNudge);
          options.additional_context = AdditionalTabContext(
              CreateAdditionalContext(web_contents.get(), selected_text),
              content::GlobalRenderFrameHostId(), PolicyCheck::kNone);
          if (!skill.id.empty()) {
            if (!skill_prompt.empty()) {
              options.prompts.push_back(skill_prompt);
            }
            options.skill_id = skill.id;
            auto mojo_skills_payload = glic::mojom::SkillsPayload::New();
            mojo_skills_payload->skill_id = skill.id;
            if (base::FeatureList::IsEnabled(
                    features::kSkillsWebViewV2Enabled)) {
              mojo_skills_payload->skill_name = skill.name;
              mojo_skills_payload->skill_icon = skill.icon;
            }
            options.source_or_payload =
                glic::mojom::InvocationPayload::NewSkillsPayload(
                    std::move(mojo_skills_payload));
            glic_keyed_service->InvokeWithAutoSubmit(
                InvokeWithAutoSubmitPasskeyProvider::GetPassKey(),
                std::move(options));
          } else {
            if (!prompt_override.empty()) {
              options.prompts.push_back(base::UTF16ToUTF8(prompt_override));
              glic_keyed_service->InvokeWithAutoSubmit(
                  InvokeWithAutoSubmitPasskeyProvider::GetPassKey(),
                  std::move(options));
            } else if (features::kGlicSelectionAutoSendPrompt.Get()) {
              std::string cta = features::kGlicSelectionPromptCta.Get();
              std::string prompt = l10n_util::GetStringUTF8(
                  IDS_GLIC_SELECTION_AUTO_SEND_PROMPT_TELL_ME);
              if (cta == features::kGlicSelectionPromptCtaExplain) {
                prompt = l10n_util::GetStringUTF8(
                    IDS_GLIC_SELECTION_AUTO_SEND_PROMPT_EXPLAIN);
              }
              options.prompts.push_back(prompt);
              glic_keyed_service->InvokeWithAutoSubmit(
                  InvokeWithAutoSubmitPasskeyProvider::GetPassKey(),
                  std::move(options));
            } else {
              glic_keyed_service->Invoke(std::move(options));
            }
          }
        }
      }
    }
  }
}

void GlicSelectionObserver::UpdateSelectionState(
    const std::u16string& selected_text,
    bool is_pending_selection) {
  last_selected_text_ = selected_text;
  auto* tab_interface =
      tabs::TabInterface::MaybeGetFromContents(web_contents());
  if (!tab_interface) {
    return;
  }
  BrowserWindowInterface* bwi = tab_interface->GetBrowserWindowInterface();

  if (selected_text.empty()) {
    if (widget_delegate_ && !is_explaining_) {
      widget_delegate_->CloseWidget();
    }

    if (has_sent_selection_context_) {
      SendAdditionalContextToPanel(tab_interface, u"");
      has_sent_selection_context_ = false;
    }

    return;
  }

  if (!bwi) {
    return;
  }

  bool panel_showing = IsPanelShowing(tab_interface, bwi);

  if (panel_showing) {
    if (is_pending_selection &&
        !features::kGlicSelectionPromptUpdatesOnly.Get()) {
      ShowSelectionAffordance(selected_text, bwi);
    } else if (widget_delegate_ && !is_explaining_) {
      widget_delegate_->CloseWidget();
    }

    SendAdditionalContextToPanel(tab_interface, selected_text);
    has_sent_selection_context_ = true;
  } else {
    if (is_pending_selection &&
        !features::kGlicSelectionPromptUpdatesOnly.Get()) {
      ShowSelectionAffordance(selected_text, bwi);
    }
    has_sent_selection_context_ = false;
  }
}

void GlicSelectionObserver::ShowSelectionAffordance(
    const std::u16string& selected_text,
    BrowserWindowInterface* bwi) {
  Profile* profile =
      Profile::FromBrowserContext(web_contents()->GetBrowserContext());
  auto* identity_manager = IdentityManagerFactory::GetForProfile(profile);
  if (!identity_manager ||
      !identity_manager->HasPrimaryAccount(signin::ConsentLevel::kSignin)) {
    return;
  }
  auto* controller = bwi->GetFeatures().glic_nudge_controller();
  if (controller) {
    bool is_post_fre = GlicEnabling::HasConsentedForProfile(
        Profile::FromBrowserContext(web_contents()->GetBrowserContext()));
    const char* histogram_suffix = is_post_fre ? ".PostFre" : ".PreFre";

    // Show selection widget
    if (!ShouldShowSelectionWidget()) {
      return;
    }
    // Find the RenderFrameHost that has the selection.
    content::RenderFrameHost* selected_frame =
        last_selection_frame_token_.has_value()
            ? content::RenderFrameHost::FromFrameToken(
                  *last_selection_frame_token_)
            : nullptr;
    if (!selected_frame) {
      return;
    }

    std::optional<gfx::Rect> bounds =
        web_contents()->GetTextSelectionBounds(selected_frame);
    if (bounds.has_value() && !bounds->IsEmpty()) {
      if (widget_delegate_) {
        widget_delegate_->CloseWidget();
      }

      base::UmaHistogramEnumeration(
          base::StrCat({"Glic.Selection.Action", histogram_suffix}),
          GlicSelectionAction::kWidgetShown);
      base::UmaHistogramCounts1000(
          base::StrCat(
              {"Glic.Selection.WidgetShown.SelectionLength", histogram_suffix}),
          selected_text.length());
      base::UmaHistogramCounts1000(
          base::StrCat({"Glic.Selection.WidgetShown.SelectionWordCount",
                        histogram_suffix}),
          CountWords(selected_text));

      widget_delegate_ = std::make_unique<GlicSelectionWidgetDelegate>(
          *action_delegate_, *bounds, web_contents()->GetContainerBounds(),
          std::u16string(selected_text));
      widget_delegate_->set_parent_window(platform_util::GetViewForWindow(
          web_contents()->GetTopLevelNativeWindow()));
      dismissal_recorded_ = false;
      widget_delegate_->ShowWidget();
      if (features::kGlicSelectionShowCopyButtons.Get()) {
        RequestLinkGeneration(selected_frame);
      }
    } else if (bounds_retry_count_ < 5) {
      // Retry showing the widget, bounds might not be available yet due
      // to IPC timing (especially on double click).
      bounds_retry_count_++;
      pending_selection_text_ = selected_text;
      base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
          FROM_HERE,
          base::BindOnce(&GlicSelectionObserver::ProcessPendingSelection,
                         weak_ptr_factory_.GetWeakPtr()),
          base::Milliseconds(100));
    }
  }
}

void GlicSelectionObserver::OnContentSettingChanged(
    const ContentSettingsPattern& primary_pattern,
    const ContentSettingsPattern& secondary_pattern,
    ContentSettingsTypeSet content_type_set) {
  if (content_type_set.Contains(ContentSettingsType::INLINE_CUE_MENU)) {
    UpdatePageBlockedState();
    if (is_site_blocked_on_current_page_ && widget_delegate_) {
      DismissUI(DismissReason::kExternal);
    }
  }
}

void GlicSelectionObserver::UpdatePageBlockedState() {
  Profile* profile =
      Profile::FromBrowserContext(web_contents()->GetBrowserContext());
  is_site_blocked_on_current_page_ =
      IsSiteBlockedForInlineCue(profile, web_contents()->GetLastCommittedURL());
}

bool GlicSelectionObserver::ShouldShowSelectionWidget() {
  return !is_hidden_on_current_page_ && !is_site_blocked_on_current_page_;
}

void GlicSelectionObserver::OnHide() {
  is_hidden_on_current_page_ = true;

  DismissUI(DismissReason::kCloseButton);
  ShowHiddenToast(ToastId::kGlicSelectionHiddenForSite);
}

void GlicSelectionObserver::ShowHiddenToast(ToastId toast_id) {
  if (auto* toast_controller =
          ToastController::MaybeGetForWebContents(web_contents())) {
    toast_controller->MaybeShowToast(ToastParams(toast_id));
  }
}

void GlicSelectionObserver::OnSettings() {
  auto* tab_interface =
      tabs::TabInterface::MaybeGetFromContents(web_contents());
  if (tab_interface) {
    BrowserWindowInterface* browser_window_interface =
        tab_interface->GetBrowserWindowInterface();
    if (browser_window_interface) {
      chrome::ShowContentSettingsExceptions(
          browser_window_interface, ContentSettingsType::INLINE_CUE_MENU);
    }
  }
}


void GlicSelectionObserver::RequestLinkGeneration(
    content::RenderFrameHost* rfh) {
  generated_link_.reset();
  if (!rfh) {
    return;
  }

  GURL url = rfh->GetMainFrame()->GetLastCommittedURL();
  if (url.has_ref()) {
    url = shared_highlighting::RemoveFragmentSelectorDirectives(url);
  }

  if (!shared_highlighting::ShouldOfferLinkToText(url)) {
    return;
  }

  text_fragment_remote_.reset();
  rfh->GetRemoteInterfaces()->GetInterface(
      text_fragment_remote_.BindNewPipeAndPassReceiver());

  text_fragment_remote_->RequestSelectorForSelection(
      base::BindOnce(&GlicSelectionObserver::OnLinkGenerated,
                     weak_ptr_factory_.GetWeakPtr(), url));
}

void GlicSelectionObserver::WriteLinkToClipboard(
    content::WeakDocumentPtr weak_document_ptr,
    const GURL& url) {
  content::RenderFrameHost* rfh = weak_document_ptr.AsRenderFrameHostIfValid();
  if (!rfh) {
    return;
  }

  enterprise_data_protection::CopyTextToClipboard(
      rfh, base::UTF8ToUTF16(url.spec()));

  if (auto* web_contents_ptr = content::WebContents::FromRenderFrameHost(rfh)) {
    shared_highlighting::LogDesktopLinkGenerationCopiedLinkType(
        shared_highlighting::LinkGenerationCopiedLinkType::
            kCopiedFromNewGeneration);

    if (toast_features::IsEnabled(
            toast_features::kLinkToHighlightCopiedToast)) {
      if (auto* tab_interface =
              tabs::TabInterface::MaybeGetFromContents(web_contents_ptr)) {
        if (auto* bwi = tab_interface->GetBrowserWindowInterface()) {
          if (auto* toast_controller = bwi->GetFeatures().toast_controller()) {
            toast_controller->MaybeShowToast(
                ToastParams(ToastId::kLinkToHighlightCopied));
          }
        }
      }
    }

    feature_engagement::TrackerFactory::GetForBrowserContext(
        web_contents_ptr->GetBrowserContext())
        ->NotifyEvent("iph_desktop_shared_highlighting_used");
  }
}

void GlicSelectionObserver::OnLinkGenerated(
    const GURL& fallback_url,
    const std::string& selector,
    shared_highlighting::LinkGenerationError error,
    shared_highlighting::LinkGenerationReadyStatus ready_status) {
  if (!selector.empty()) {
    generated_link_ =
        shared_highlighting::AppendSelectors(fallback_url, {selector});
  }
  if (widget_delegate_) {
    widget_delegate_->UpdateCopyLinkButton(generated_link_.has_value());
  }
}

bool GlicSelectionObserver::IsPanelShowing(tabs::TabInterface* tab_interface,
                                           BrowserWindowInterface* bwi) {
  if (glic_keyed_service_ &&
      glic_keyed_service_->GetInstanceForTab(tab_interface)) {
    return glic_keyed_service_->IsPanelShowingForBrowser(*bwi);
  }
  return false;
}

void GlicSelectionObserver::SendAdditionalContextToPanel(
    tabs::TabInterface* tab_interface,
    const std::u16string& selected_text) {
  if (!glic_keyed_service_) {
    return;
  }

  // If the page is not eligible, do not send the additional context.
  if (!IsPageContextEligible() && !selected_text.empty()) {
    return;
  }

  GlicInvokeOptions options(glic::Target(*tab_interface, DefaultConversation()),
                            mojom::InvocationSource::kTextSelectionWidget);
  options.additional_context = AdditionalTabContext(
      CreateAdditionalContext(web_contents(), selected_text),
      content::GlobalRenderFrameHostId(), PolicyCheck::kNone);
  glic_keyed_service_->Invoke(std::move(options));
}

bool GlicSelectionObserver::IsPageContextEligible() const {
  auto* tab_interface =
      tabs::TabInterface::MaybeGetFromContents(web_contents());
  if (tab_interface) {
    auto* helper = tabs::PageContextEligibilityHelper::From(tab_interface);
    if (helper) {
      return helper->IsPageContextEligible() ==
             optimization_guide::PageContextEligibilityStatus::kEligible;
    }
  }
  if (page_context_tracker_) {
    return page_context_tracker_->IsPageContextEligible() ==
           optimization_guide::PageContextEligibilityStatus::kEligible;
  }
  return false;
}

void GlicSelectionObserver::OnPageContextEligibilityChanged(
    optimization_guide::PageContextEligibilityStatus status) {
  // If the page context transitions into a liminal state (kUnknown) or becomes
  // ineligible (kNotEligible), and we've already sent selection context, we
  // should clear it.
  if (status != optimization_guide::PageContextEligibilityStatus::kEligible &&
      has_sent_selection_context_) {
    auto* tab_interface =
        tabs::TabInterface::MaybeGetFromContents(web_contents());
    if (tab_interface) {
      SendAdditionalContextToPanel(tab_interface, std::u16string());
      has_sent_selection_context_ = false;
    }
  } else if (status ==
                 optimization_guide::PageContextEligibilityStatus::kEligible &&
             !last_selected_text_.empty()) {
    auto* tab_interface =
        tabs::TabInterface::MaybeGetFromContents(web_contents());
    if (!tab_interface) {
      return;
    }
    BrowserWindowInterface* bwi = tab_interface->GetBrowserWindowInterface();
    if (bwi && IsPanelShowing(tab_interface, bwi)) {
      SendAdditionalContextToPanel(tab_interface, last_selected_text_);
      has_sent_selection_context_ = true;
    }
  }
}

void GlicSelectionObserver::CreatePageContextEligibilityAPI(
    std::string account) {
  base::ThreadPool::PostTaskAndReplyWithResult(
      FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()},
      base::BindOnce(&optimization_guide::PageContextEligibility::Get),
      base::BindOnce(&GlicSelectionObserver::OnPageContextEligibilityAPILoaded,
                     weak_ptr_factory_.GetWeakPtr(), std::move(account)));
}

void GlicSelectionObserver::OnPageContextEligibilityAPILoaded(
    std::string account,
    optimization_guide::PageContextEligibility* page_context_eligibility) {
  if (!page_context_eligibility) {
    return;
  }
  page_context_tracker_ =
      optimization_guide::PageContextEligibilityObserver::Create(
          web_contents(), std::move(account),
          base::BindRepeating(
              [](base::WeakPtr<GlicSelectionObserver> observer,
                 optimization_guide::PageContextEligibilityStatus status) {
                if (observer) {
                  observer->OnPageContextEligibilityChanged(status);
                }
              },
              weak_ptr_factory_.GetWeakPtr()));
}

void GlicSelectionObserver::CopyLinkToHighlight(
    content::WeakDocumentPtr weak_document_ptr) {
  if (generated_link_.has_value() && generated_link_->is_valid()) {
    WriteLinkToClipboard(weak_document_ptr, generated_link_.value());
  }
}

void GlicSelectionObserver::OnGlobalPanelShowHide() {
  if (last_selected_text_.empty()) {
    return;
  }

  UpdateSelectionState(last_selected_text_, /*is_pending_selection=*/false);
}

void GlicSelectionObserver::OnAskGemini() {
  if (ExplainSelectionTrigger::IsInlineFulfillmentSupported()) {
    is_explaining_ = true;
    if (explain_selection_trigger_) {
      explain_selection_trigger_->RequestExplanation(
          web_contents(), base::UTF16ToUTF8(last_selected_text_),
          /*surrounding_text=*/"",
          base::BindRepeating(&GlicSelectionObserver::OnInlineExplanationUpdate,
                              weak_ptr_factory_.GetWeakPtr()));
    }
    return;
  }
  DismissUI(DismissReason::kActionTaken);
  InvokeGlicFromSelectionAffordance(last_selected_text_, /*is_widget=*/true,
                                    web_contents()->GetWeakPtr(),
                                    GlicNudgeActivity::kNudgeClicked);
}

void GlicSelectionObserver::OnAskGeminiWithSkill(
    const GlicSelectionWidgetDelegate::SkillOption& skill) {
  if (!features::kGlicSelectionPromptSkills.Get() || skill.id.empty()) {
    return;
  }

  std::string skill_prompt;
  auto* tab_interface =
      tabs::TabInterface::MaybeGetFromContents(web_contents());
  if (tab_interface) {
    if (auto* observer = skills::SkillsUpdateObserver::From(tab_interface)) {
      if (const auto* list = observer->contextual_skills()) {
        for (const auto& s : list->skills()) {
          if (s.id() == skill.id) {
            skill_prompt = s.prompt();
            break;
          }
        }
      }
    }
  }

  if (skill_prompt.empty()) {
    Profile* profile =
        Profile::FromBrowserContext(web_contents()->GetBrowserContext());
    if (auto* service =
            skills::SkillsServiceFactory::GetForProfile(profile)) {
      if (const auto* s = service->GetSkillById(skill.id)) {
        skill_prompt = s->prompt;
      }
    }
  }

  DismissUI(DismissReason::kActionTaken);
  InvokeGlicFromSelectionAffordance(
      last_selected_text_, /*is_widget=*/true, web_contents()->GetWeakPtr(),
      GlicNudgeActivity::kNudgeClicked, /*prompt_override=*/u"", skill,
      skill_prompt);
}

std::vector<GlicSelectionWidgetDelegate::SkillOption>
GlicSelectionObserver::GetContextualSkills() {
  std::vector<GlicSelectionWidgetDelegate::SkillOption> result;
  if (!features::kGlicSelectionPromptSkills.Get()) {
    return result;
  }
  auto* tab_interface =
      tabs::TabInterface::MaybeGetFromContents(web_contents());
  if (!tab_interface) {
    return result;
  }
  auto* observer = skills::SkillsUpdateObserver::From(tab_interface);
  if (!observer) {
    return result;
  }
  const auto* list = observer->contextual_skills();
  if (!list) {
    return result;
  }
  for (const auto& skill : list->skills()) {
    if (!skill.id().empty() && !skill.name().empty()) {
      result.emplace_back(
          skills::Skill(skill.id(), skill.name(), skill.icon(), ""));
    }
  }
  return result;
}

std::vector<GlicSelectionWidgetDelegate::SkillOption>
GlicSelectionObserver::GetUserSkills() {
  std::vector<GlicSelectionWidgetDelegate::SkillOption> result;
  if (!features::kGlicSelectionPromptSkills.Get()) {
    return result;
  }
  Profile* profile =
      Profile::FromBrowserContext(web_contents()->GetBrowserContext());
  if (!profile) {
    return result;
  }
  auto* service = skills::SkillsServiceFactory::GetForProfile(profile);
  if (!service) {
    return result;
  }
  base::flat_set<std::string> contextual_ids;
  for (const auto& option : GetContextualSkills()) {
    contextual_ids.insert(option.id);
  }
  for (const auto& skill : service->GetSkills()) {
    if (skill && !skill->id.empty() && !skill->name.empty() &&
        !contextual_ids.contains(skill->id)) {
      result.push_back(*skill);
    }
  }
  return result;
}

void GlicSelectionObserver::OnAskGeminiForQuery(const std::u16string& query) {
  last_selected_text_ = query;
  if (ExplainSelectionTrigger::IsInlineFulfillmentSupported()) {
    is_explaining_ = true;
    if (explain_selection_trigger_) {
      explain_selection_trigger_->RequestExplanation(
          web_contents(), base::UTF16ToUTF8(query),
          /*surrounding_text=*/"",
          base::BindRepeating(&GlicSelectionObserver::OnInlineExplanationUpdate,
                              weak_ptr_factory_.GetWeakPtr()));
    }
    return;
  }
  DismissUI(DismissReason::kActionTaken);
  InvokeGlicFromSelectionAffordance(query, /*is_widget=*/true,
                                    web_contents()->GetWeakPtr(),
                                    GlicNudgeActivity::kNudgeClicked);
}

void GlicSelectionObserver::OnAskGeminiMoreAboutThis(
    const std::u16string& selected_text,
    const std::string& explanation_text) {
  is_explaining_ = false;
  DismissUI(DismissReason::kActionTaken);
  std::u16string prompt = selected_text;
  if (!selected_text.starts_with(u"Tell me more about") &&
      selected_text == last_selected_text_) {
    prompt = u"Tell me more about \"" + selected_text + u"\"";
  }
  if (!explanation_text.empty()) {
    prompt += u"\n\nContext:\n" + base::UTF8ToUTF16(explanation_text);
  }
  InvokeGlicFromSelectionAffordance(
      last_selected_text_, /*is_widget=*/true,
      web_contents()->GetWeakPtr(),
      GlicNudgeActivity::kNudgeClicked,
      /*prompt_override=*/prompt);
}

void GlicSelectionObserver::OnInlineExplanationUpdate(
    const std::string& markdown_output,
    bool is_complete,
    const std::string& error_message) {
  if (widget_delegate_) {
    widget_delegate_->ShowInlineExplanation(markdown_output, is_complete,
                                            error_message);
  }
}

void GlicSelectionObserver::OnCopy() {
  DismissUI(DismissReason::kActionTaken);
  web_contents()->Copy();
}

void GlicSelectionObserver::OnCopyLink() {
  DismissUI(DismissReason::kActionTaken);
  content::RenderFrameHost* selected_frame =
      last_selection_frame_token_.has_value()
          ? content::RenderFrameHost::FromFrameToken(
                *last_selection_frame_token_)
          : nullptr;
  if (selected_frame) {
    CopyLinkToHighlight(selected_frame->GetWeakDocumentPtr());
  }
}

void GlicSelectionObserver::OnOpenInSidePanel() {
  DismissUI(DismissReason::kActionTaken);
  InvokeGlicFromSelectionAffordance(last_selected_text_, /*is_widget=*/true,
                                    web_contents()->GetWeakPtr(),
                                    GlicNudgeActivity::kNudgeClicked);
}

void GlicSelectionObserver::OnWidgetClose() {
  if (widget_delegate_) {
    // Defer the destruction of the delegate to ensure the views::Widget is
    // destroyed first, and then the delegate. This is required under the
    // CLIENT_OWNS_WIDGET ownership model.
    base::SingleThreadTaskRunner::GetCurrentDefault()->DeleteSoon(
        FROM_HERE, std::move(widget_delegate_));
  }
}

bool GlicSelectionObserver::IsShakeTriggerEnabled() const {
  if (!base::FeatureList::IsEnabled(features::kGlicShakeTrigger)) {
    return false;
  }
  if (!web_contents()) {
    return false;
  }
  Profile* profile =
      Profile::FromBrowserContext(web_contents()->GetBrowserContext());
  if (!profile || !profile->GetPrefs()) {
    return false;
  }
  return profile->GetPrefs()->GetBoolean(prefs::kGlicShakeTriggerEnabled);
}

void GlicSelectionObserver::TriggerRegionCapture() {
  if (!IsSelectionPromptEnabled() || !IsShakeTriggerEnabled()) {
    return;
  }
  if (!glic_keyed_service_) {
    return;
  }
  auto* tab_interface =
      tabs::TabInterface::MaybeGetFromContents(web_contents());
  if (!tab_interface) {
    return;
  }
  GlicInvokeOptions options(
      Target(*tab_interface),
      glic::mojom::InvocationSource::kCaptureRegionHotkey);
  options.wait_for_panel_open = true;
  glic_keyed_service_->Invoke(std::move(options));
}

void GlicSelectionObserver::ProcessMouseMoveForShake(
    const blink::WebMouseEvent& mouse_event) {
  if (!IsShakeTriggerEnabled()) {
    return;
  }
  if (direction_change_count_ > 0 &&
      (base::TimeTicks::Now() - last_direction_change_time_) > kShakeTimeout) {
    ResetShakeDetector();
  }

  gfx::PointF current_pos = mouse_event.PositionInWidget();

  if (!last_shake_point_.has_value()) {
    last_shake_point_ = current_pos;
    return;
  }

  gfx::Vector2dF delta = current_pos - *last_shake_point_;
  float dist = delta.Length();
  if (dist < kMinShakeDistance) {
    return;
  }

  gfx::Vector2dF current_dir(delta.x() / dist, delta.y() / dist);

  if (!last_shake_dir_.has_value()) {
    last_shake_dir_ = current_dir;
    last_shake_point_ = current_pos;
    last_direction_change_time_ = base::TimeTicks::Now();
    return;
  }

  float dot = last_shake_dir_->x() * current_dir.x() +
              last_shake_dir_->y() * current_dir.y();
  if (dot < -0.5f) {
    direction_change_count_++;
    last_shake_dir_ = current_dir;
    last_shake_point_ = current_pos;
    last_direction_change_time_ = base::TimeTicks::Now();

    if (direction_change_count_ >= kRequiredDirectionChanges) {
      ResetShakeDetector();
      TriggerRegionCapture();
    }
  } else {
    last_shake_point_ = current_pos;
  }
}

void GlicSelectionObserver::ResetShakeDetector() {
  last_shake_point_.reset();
  last_shake_dir_.reset();
  direction_change_count_ = 0;
  last_direction_change_time_ = base::TimeTicks();
}

}  // namespace glic
