// 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 "chrome/browser/ui/exclusive_access/keyboard_lock_controller.h"

#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/user_metrics.h"
#include "base/time/default_tick_clock.h"
#include "base/time/time.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_bubble_hide_callback.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_permission_manager.h"
#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
#include "chrome/browser/ui/ui_features.h"
#include "components/input/native_web_keyboard_event.h"
#include "components/permissions/features.h"
#include "content/public/browser/web_contents.h"
#include "ui/events/keycodes/keyboard_codes.h"

using base::TimeTicks;
using content::WebContents;

namespace {

// Amount of time the user must hold ESC to exit full screen.
constexpr base::TimeDelta kHoldEscapeTime = base::Milliseconds(1500);

// Amount of time to look for ESC key presses to reshow the exit instructions.
constexpr base::TimeDelta kDefaultEscRepeatWindow = base::Seconds(1);

// Number of times ESC must be pressed within |kDefaultEscRepeatWindow| to
// trigger the exit instructions to be shown again.
constexpr int kEscRepeatCountToTriggerUiReshow = 3;

// Check whether `event` is a kRawKeyDown type and doesn't have non-stateful
// modifiers (i.e. shift, ctrl etc.).
bool IsUnmodifiedEscKeyDownEvent(const input::NativeWebKeyboardEvent& event) {
  if (event.GetType() != input::NativeWebKeyboardEvent::Type::kRawKeyDown) {
    return false;
  }
  if (event.GetModifiers() & blink::WebInputEvent::kKeyModifiers) {
    return false;
  }
  return true;
}

}  // namespace

KeyboardLockController::KeyboardLockController(ExclusiveAccessManager* manager)
    : ExclusiveAccessControllerBase(manager),
      esc_repeat_window_(kDefaultEscRepeatWindow),
      esc_repeat_tick_clock_(base::DefaultTickClock::GetInstance()) {}

KeyboardLockController::~KeyboardLockController() = default;

bool KeyboardLockController::HandleUserPressedEscape() {
  if (!IsKeyboardLockActive() || RequiresPressAndHoldEscToExit()) {
    return false;
  }

  base::RecordAction(base::UserMetricsAction("UnlockKeyboard_PressEsc"));
  UnlockKeyboard();
  return true;
}

void KeyboardLockController::HandleUserHeldEscape() {
  if (!IsKeyboardLockActive()) {
    return;
  }

  base::RecordAction(base::UserMetricsAction("UnlockKeyboard_PressAndHoldEsc"));
  UnlockKeyboard();
}

void KeyboardLockController::HandleUserReleasedEscapeEarly() {
  if (RequiresPressAndHoldEscToExit()) {
    ReShowExitBubbleIfNeeded();
  }
}

bool KeyboardLockController::RequiresPressAndHoldEscToExit() const {
  DCHECK_EQ(keyboard_lock_state_ == KeyboardLockState::kUnlocked,
            exclusive_access_tab() == nullptr);
  return keyboard_lock_state_ == KeyboardLockState::kLockedWithEsc;
}

void KeyboardLockController::ExitExclusiveAccessToPreviousState() {
  UnlockKeyboard();
}

void KeyboardLockController::ExitExclusiveAccessIfNecessary() {
  UnlockKeyboard();
}

void KeyboardLockController::NotifyTabExclusiveAccessLost() {
  UnlockKeyboard();
}

bool KeyboardLockController::IsKeyboardLockActive() const {
  DCHECK_EQ(keyboard_lock_state_ == KeyboardLockState::kUnlocked,
            exclusive_access_tab() == nullptr);
  return keyboard_lock_state_ != KeyboardLockState::kUnlocked;
}

void KeyboardLockController::RequestKeyboardLock(WebContents* web_contents,
                                                 bool esc_key_locked) {
  DCHECK(!exclusive_access_tab() || exclusive_access_tab() == web_contents);
  if (!base::FeatureList::IsEnabled(
          permissions::features::kKeyboardLockPrompt)) {
    LockKeyboard(web_contents->GetWeakPtr(), esc_key_locked);
    return;
  }
  exclusive_access_manager()->permission_manager().QueuePermissionRequest(
      blink::PermissionType::KEYBOARD_LOCK,
      base::BindOnce(&KeyboardLockController::LockKeyboard,
                     weak_ptr_factory_.GetWeakPtr(), web_contents->GetWeakPtr(),
                     esc_key_locked),
      base::BindOnce(&KeyboardLockController::UnlockKeyboardForWebContents,
                     weak_ptr_factory_.GetWeakPtr(),
                     web_contents->GetWeakPtr()),
      web_contents);
}

bool KeyboardLockController::HandleKeyEvent(
    const input::NativeWebKeyboardEvent& event) {
  if (base::FeatureList::IsEnabled(
          features::kPressAndHoldEscToExitBrowserFullscreen)) {
    return false;
  }

  DCHECK_EQ(ui::VKEY_ESCAPE, event.windows_key_code);
  // This method handles the press and hold gesture used for exiting fullscreen.
  // If we don't have a feature which requires press and hold, or there isn't an
  // active keyboard lock request which requires press and hold, then we just
  // return as the simple 'press esc to exit' case is handled by the caller
  // (which is the ExclusiveAccessManager in this case).
  if (!RequiresPressAndHoldEscToExit()) {
    return false;
  }

  // Note: This logic handles exiting fullscreen but the UI feedback element is
  // created and managed by the FullscreenControlHost class.
  if (event.GetType() == input::NativeWebKeyboardEvent::Type::kKeyUp &&
      hold_timer_.IsRunning()) {
    // Seeing a key up event on Esc with the hold timer running cancels the
    // timer and doesn't exit. This means the user pressed Esc, but not long
    // enough to trigger an exit
    hold_timer_.Stop();
    ReShowExitBubbleIfNeeded();
  } else if (IsUnmodifiedEscKeyDownEvent(event) && !hold_timer_.IsRunning()) {
    // Seeing a key down event on Esc when the hold timer is stopped starts
    // the timer. When the timer fires, the callback will trigger an exit from
    // fullscreen/pointerlock/keyboardlock.
    hold_timer_.Start(
        FROM_HERE, kHoldEscapeTime,
        base::BindOnce(&KeyboardLockController::HandleUserHeldEscapeDeprecated,
                       base::Unretained(this)));
  }

  return true;
}

void KeyboardLockController::CancelKeyboardLockRequest(WebContents* tab) {
  if (tab == exclusive_access_tab()) {
    UnlockKeyboard();
  }
}

void KeyboardLockController::LockKeyboard(
    base::WeakPtr<content::WebContents> web_contents,
    bool esc_key_locked) {
  if (!web_contents) {
    NotifyLockRequestResult();
    return;
  }
  // Call GotResponseToKeyboardLockRequest() to notify `web_contents` of the
  // result, regardless of the fullscreen state.
  if (!web_contents->GotResponseToKeyboardLockRequest(true) ||
      !web_contents->IsFullscreen()) {
    UnlockKeyboard();
    return;
  }
  KeyboardLockState new_lock_state = esc_key_locked
                                         ? KeyboardLockState::kLockedWithEsc
                                         : KeyboardLockState::kLockedWithoutEsc;
  // Only re-show the exit bubble if the requesting web_contents has changed
  // (or is new) or if the esc key lock state has changed.
  bool reshow_exit_bubble = exclusive_access_tab() != web_contents.get() ||
                            new_lock_state != keyboard_lock_state_;
  keyboard_lock_state_ = new_lock_state;
  SetTabWithExclusiveAccess(web_contents.get());
  if (reshow_exit_bubble) {
    exclusive_access_manager()->UpdateBubble(
        bubble_hide_callback_for_test_
            ? base::BindOnce(bubble_hide_callback_for_test_)
            : base::NullCallback());
  }
  NotifyLockRequestResult();
}

void KeyboardLockController::UnlockKeyboard() {
  UnlockKeyboardForWebContents(
      exclusive_access_tab() ? exclusive_access_tab()->GetWeakPtr() : nullptr);
}

void KeyboardLockController::UnlockKeyboardForWebContents(
    base::WeakPtr<content::WebContents> web_contents) {
  if (!web_contents) {
    return;
  }

  keyboard_lock_state_ = KeyboardLockState::kUnlocked;

  web_contents->GotResponseToKeyboardLockRequest(false);
  SetTabWithExclusiveAccess(nullptr);
  exclusive_access_manager()->UpdateBubble(base::NullCallback());
  NotifyLockRequestResult();
}

void KeyboardLockController::HandleUserHeldEscapeDeprecated() {
  if (base::FeatureList::IsEnabled(
          features::kPressAndHoldEscToExitBrowserFullscreen)) {
    return;
  }

  ExclusiveAccessManager* const manager = exclusive_access_manager();
  manager->fullscreen_controller()->HandleUserPressedEscape();
  manager->pointer_lock_controller()->HandleUserPressedEscape();
  HandleUserPressedEscape();
  base::RecordAction(base::UserMetricsAction("UnlockKeyboard_PressAndHoldEsc"));
}

void KeyboardLockController::ReShowExitBubbleIfNeeded() {
  TimeTicks now = esc_repeat_tick_clock_->NowTicks();
  TimeTicks esc_repeat_window = now - esc_repeat_window_;
  // Remove any events which are outside of the window.
  while (!esc_keypress_tracker_.empty() &&
         esc_keypress_tracker_.front() < esc_repeat_window) {
    esc_keypress_tracker_.pop_front();
  }

  esc_keypress_tracker_.push_back(now);
  if (esc_keypress_tracker_.size() >= kEscRepeatCountToTriggerUiReshow) {
    exclusive_access_manager()->UpdateBubble(base::NullCallback(),
                                             /*force_update=*/true);
    esc_keypress_tracker_.clear();

    if (esc_repeat_triggered_for_test_) {
      std::move(esc_repeat_triggered_for_test_).Run();
    }
  }
}

void KeyboardLockController::NotifyLockRequestResult() {
  if (lock_state_callback_for_test_) {
    std::move(lock_state_callback_for_test_).Run();
  }
}
