// Copyright 2015 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/exclusive_access_manager.h"

#include <utility>

#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/metrics/histogram_functions.h"
#include "build/build_config.h"
#include "chrome/browser/app_mode/app_mode_utils.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_context.h"
#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
#include "chrome/browser/ui/exclusive_access/pointer_lock_controller.h"
#include "chrome/browser/ui/ui_features.h"
#include "components/input/native_web_keyboard_event.h"
#include "content/public/browser/web_contents.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "services/metrics/public/cpp/ukm_recorder.h"
#include "ui/events/keycodes/keyboard_codes.h"

using content::WebContents;

namespace {

// Amount of time the user must press on Esc to make it a press-and-hold event.
constexpr base::TimeDelta kHoldEscapeTime = base::Milliseconds(1500);

// Amount of time the user must press on Esc to see the Exclusive Access Bubble
// showing up.
constexpr base::TimeDelta kShowExitBubbleTime = base::Milliseconds(500);

constexpr char kHistogramEscKeyPressedDownWithModifier[] =
    "Browser.EscKeyPressedDownWithModifier";

// Check whether `event` is a kKeyDown or 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 &&
      event.GetType() != input::NativeWebKeyboardEvent::Type::kKeyDown) {
    return false;
  }
  if (event.GetModifiers() & blink::WebInputEvent::kKeyModifiers) {
    return false;
  }
  return true;
}

}  // namespace

DEFINE_USER_DATA(ExclusiveAccessManager);

// static
ExclusiveAccessManager* ExclusiveAccessManager::From(
    BrowserWindowInterface* browser) {
  return Get(browser->GetUnownedUserDataHost());
}

// static
const ExclusiveAccessManager* ExclusiveAccessManager::From(
    const BrowserWindowInterface* browser) {
  return Get(browser->GetUnownedUserDataHost());
}

ExclusiveAccessManager::ExclusiveAccessManager(
    BrowserWindowInterface* browser,
    ExclusiveAccessContext* exclusive_access_context,
    chrome::BrowserCommandController* browser_command_controller,
    BookmarkBarController* bookmark_bar_controller)
    : exclusive_access_context_(exclusive_access_context),
      fullscreen_controller_(this,
                             browser_command_controller,
                             bookmark_bar_controller),
      keyboard_lock_controller_(this),
      pointer_lock_controller_(this),
      exclusive_access_controllers_({&fullscreen_controller_,
                                     &keyboard_lock_controller_,
                                     &pointer_lock_controller_}),
      permission_manager_(exclusive_access_context) {
  if (browser) {
    scoped_unowned_user_data_.emplace(browser->GetUnownedUserDataHost(), *this);
  }
}

ExclusiveAccessManager::ExclusiveAccessManager(
    ExclusiveAccessContext* exclusive_access_context)
    : ExclusiveAccessManager(nullptr,
                             exclusive_access_context,
                             nullptr,
                             nullptr) {}

ExclusiveAccessManager::~ExclusiveAccessManager() = default;

ExclusiveAccessBubbleType
ExclusiveAccessManager::GetExclusiveAccessExitBubbleType() const {
  // In kiosk and exclusive app mode we always want to be fullscreen and do not
  // want to show exit instructions for browser mode fullscreen.
  bool app_mode = false;
#if !BUILDFLAG(IS_MAC)  // App mode (kiosk) is not available on Mac yet.
  app_mode = IsRunningInAppMode();
#endif

  if (fullscreen_controller_.IsWindowFullscreenForTabOrPending()) {
    if (!fullscreen_controller_.IsTabFullscreen()) {
      return EXCLUSIVE_ACCESS_BUBBLE_TYPE_FULLSCREEN_EXIT_INSTRUCTION;
    }

    if (pointer_lock_controller_.IsPointerLockedSilently()) {
      return EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE;
    }

    if (keyboard_lock_controller_.RequiresPressAndHoldEscToExit()) {
      return EXCLUSIVE_ACCESS_BUBBLE_TYPE_KEYBOARD_LOCK_EXIT_INSTRUCTION;
    }

    if (pointer_lock_controller_.IsPointerLocked()) {
      return EXCLUSIVE_ACCESS_BUBBLE_TYPE_FULLSCREEN_POINTERLOCK_EXIT_INSTRUCTION;
    }

    return EXCLUSIVE_ACCESS_BUBBLE_TYPE_FULLSCREEN_EXIT_INSTRUCTION;
  }

  if (pointer_lock_controller_.IsPointerLockedSilently()) {
    return EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE;
  }

  if (pointer_lock_controller_.IsPointerLocked()) {
    return EXCLUSIVE_ACCESS_BUBBLE_TYPE_POINTERLOCK_EXIT_INSTRUCTION;
  }

  if (fullscreen_controller_.IsExtensionFullscreenOrPending()) {
    return EXCLUSIVE_ACCESS_BUBBLE_TYPE_EXTENSION_FULLSCREEN_EXIT_INSTRUCTION;
  }

  if (fullscreen_controller_.IsControllerInitiatedFullscreen() && !app_mode) {
    return EXCLUSIVE_ACCESS_BUBBLE_TYPE_BROWSER_FULLSCREEN_EXIT_INSTRUCTION;
  }

  return EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE;
}

void ExclusiveAccessManager::UpdateBubble(
    ExclusiveAccessBubbleHideCallback first_hide_callback,
    bool force_update) {
  exclusive_access_context_->UpdateExclusiveAccessBubble(
      {.origin = GetExclusiveAccessBubbleOrigin(),
       .type = GetExclusiveAccessExitBubbleType(),
       .force_update = force_update},
      std::move(first_hide_callback));
}

url::Origin ExclusiveAccessManager::GetExclusiveAccessBubbleOrigin() const {
  url::Origin result =
      fullscreen_controller_.GetOriginForExclusiveAccessBubble();
  if (result.opaque()) {
    result = pointer_lock_controller_.GetOriginForExclusiveAccessBubble();
  }
  return result;
}



void ExclusiveAccessManager::OnTabDeactivated(WebContents* web_contents) {
  auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
  for (auto controller : exclusive_access_controllers_) {
    controller->OnTabDeactivated(web_contents);
    if (!weak_ptr) {
      return;
    }
  }
}

void ExclusiveAccessManager::OnTabDetachedFromView(WebContents* web_contents) {
  auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
  for (auto controller : exclusive_access_controllers_) {
    controller->OnTabDetachedFromView(web_contents);
    if (!weak_ptr) {
      return;
    }
  }
}

void ExclusiveAccessManager::OnTabClosing(WebContents* web_contents) {
  auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
  for (auto controller : exclusive_access_controllers_) {
    controller->OnTabClosing(web_contents);
    if (!weak_ptr) {
      return;
    }
  }
}

bool ExclusiveAccessManager::HandleUserKeyEvent(
    const input::NativeWebKeyboardEvent& event) {
  if (event.windows_key_code != ui::VKEY_ESCAPE) {
    OnUserInput();
    return false;
  }

  auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
  // When `features::kPressAndHoldEscToExitBrowserFullscreen` is enabled, the
  // `esc_key_hold_timer_` starts on `kRawKeyDown` events, unless the key press
  // event comes with a modifier key. This metrics records how often the timer
  // does not start due to using the modifier key.
  if (event.GetType() == input::NativeWebKeyboardEvent::Type::kRawKeyDown) {
    base::UmaHistogramBoolean(
        kHistogramEscKeyPressedDownWithModifier,
        event.GetModifiers() != blink::WebInputEvent::kNoModifiers);
  }

  if (base::FeatureList::IsEnabled(
          features::kPressAndHoldEscToExitBrowserFullscreen)) {
    if (event.GetType() == input::NativeWebKeyboardEvent::Type::kKeyUp &&
        esc_key_hold_timer_.IsRunning()) {
      esc_key_hold_timer_.Stop();
      show_exit_bubble_timer_.Stop();
      for (auto controller : exclusive_access_controllers_) {
        controller->HandleUserReleasedEscapeEarly();
        if (!weak_ptr) {
          return false;
        }
      }
    } else if (IsUnmodifiedEscKeyDownEvent(event) &&
               !esc_key_hold_timer_.IsRunning()) {
      esc_key_hold_timer_.Start(
          FROM_HERE, kHoldEscapeTime,
          base::BindOnce(&ExclusiveAccessManager::HandleUserHeldEscape,
                         base::Unretained(this)));
      show_exit_bubble_timer_.Start(
          FROM_HERE, kShowExitBubbleTime,
          base::BindOnce(&ExclusiveAccessManager::UpdateBubble,
                         base::Unretained(this), base::NullCallback(),
                         /*force_update=*/true));
    }
    // If the keyboard lock is enabled and requires press-and-hold Esc to exit,
    // do not pass the event to other controllers. Returns false as we don't
    // want to prevent the event from propagating to the webpage.
    if (keyboard_lock_controller_.RequiresPressAndHoldEscToExit()) {
      return false;
    }
  } else {
    // Give the `keyboard_lock_controller_` first chance at handling the Esc
    // event as there are specific UX behaviors that occur when that mode is
    // active which are coordinated by that class.  Return false as we don't
    // want to prevent the event from propagating to the webpage.
    if (keyboard_lock_controller_.HandleKeyEvent(event)) {
      return false;
    }
  }

  bool handled = false;
  for (auto controller : exclusive_access_controllers_) {
    if (controller->HandleUserPressedEscape()) {
      handled = true;
    }
    if (!weak_ptr) {
      return handled;
    }
  }
  return handled;
}

void ExclusiveAccessManager::OnUserInput() {
  exclusive_access_context_->OnExclusiveAccessUserInput();
}

void ExclusiveAccessManager::ExitExclusiveAccess() {
  auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
  for (auto controller : exclusive_access_controllers_) {
    controller->ExitExclusiveAccessToPreviousState();
    if (!weak_ptr) {
      return;
    }
  }
}

void ExclusiveAccessManager::HandleUserHeldEscape() {
  auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
  for (auto controller : exclusive_access_controllers_) {
    controller->HandleUserHeldEscape();
    if (!weak_ptr) {
      return;
    }
  }
}
