// Copyright 2017 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/views/fullscreen_control/fullscreen_control_host.h"

#include "base/check_deref.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/user_metrics.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/app_mode/app_mode_utils.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/exclusive_access/exclusive_access_context.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/fullscreen/browser_window_fullscreen_controller.h"
#include "chrome/browser/ui/views/exclusive_access/exclusive_access_bubble_views.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "ui/events/event.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/events/types/event_type.h"
#include "ui/views/event_monitor.h"
#include "ui/views/view.h"

namespace {

// +----------------------------+
// |         +-------+          |
// |         |Control|          |
// |         +-------+          |
// |                            | <-- Control.bottom * kExitHeightScaleFactor
// |          Screen            |       Buffer for mouse moves or pointer events
// |                            |       before closing the fullscreen exit
// |                            |       control.
// +----------------------------+
//
// The same value is also used for timeout cooldown.
// This is a common scenario where people play video or present slides and they
// just want to keep their cursor on the top. In this case we timeout the exit
// control so that it doesn't show permanently. The user will then need to move
// the cursor out of the cooldown area and move it back to the top to re-trigger
// the exit UI.
constexpr float kExitHeightScaleFactor = 1.5f;

// +----------------------------+
// |                            |
// |                            |
// |                            | <-- kShowFullscreenExitControlHeight
// |          Screen            |       If a mouse move or pointer event is
// |                            |       above this line, show the fullscreen
// |                            |       exit control.
// |                            |
// +----------------------------+
constexpr float kShowFullscreenExitControlHeight = 3.f;

// Time to wait to hide the popup after it is triggered.
constexpr base::TimeDelta kMousePopupTimeout = base::Seconds(3);
constexpr base::TimeDelta kTouchPopupTimeout = base::Seconds(10);

// Time to wait before showing the popup when the escape key is held.
constexpr base::TimeDelta kKeyPressPopupDelay = base::Seconds(1);

bool IsExitUiEnabled() {
#if BUILDFLAG(IS_MAC)
  // Exit UI is unnecessary, since Mac uses the OS fullscreen such that window
  // menu and controls reveal when the cursor is moved to the top.
  return false;
#else
  // Kiosk mode is a fullscreen experience, which makes the exit UI
  // inappropriate.
  return !IsRunningInAppMode();
#endif
}

}  // namespace

DEFINE_USER_DATA(FullscreenControlHost);

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

FullscreenControlHost::FullscreenControlHost(
    BrowserView* browser_view,
    ExclusiveAccessManager* exclusive_access_manager,
    ui::UnownedUserDataHost& host)
    : scoped_unowned_user_data_(host, *this),
      browser_view_(browser_view),
      exclusive_access_manager_(CHECK_DEREF(exclusive_access_manager)) {}

FullscreenControlHost::~FullscreenControlHost() = default;

// static
bool FullscreenControlHost::IsFullscreenExitUIEnabled() {
  // TODO(joedow): Remove this function and all uses of it. The fullscreen exit
  // UI is now always enabled because the keyboard lock UI is always enabled.
  return true;
}

void FullscreenControlHost::OnEvent(const ui::Event& event) {
  if (event.IsKeyEvent()) {
    OnKeyEvent(*event.AsKeyEvent());
  } else if (event.IsMouseEvent()) {
    OnMouseEvent(*event.AsMouseEvent());
  } else if (event.IsTouchEvent()) {
    OnTouchEvent(*event.AsTouchEvent());
  } else if (event.IsGestureEvent()) {
    OnGestureEvent(*event.AsGestureEvent());
  }
}

void FullscreenControlHost::OnKeyEvent(const ui::KeyEvent& event) {
  if (event.key_code() != ui::VKEY_ESCAPE ||
      (input_entry_method_ != InputEntryMethod::NOT_ACTIVE &&
       input_entry_method_ != InputEntryMethod::KEYBOARD)) {
    return;
  }

  // FullscreenControlHost UI is not needed for the keyboard input method in any
  // fullscreen mode except for tab-initiated fullscreen (and only when the user
  // is required to press and hold the escape key to exit).

  // If we are not in tab-initiated fullscreen, then we want to make sure the
  // UI exit bubble is not displayed.  This can occur when:
  // 1.) The user enters browser fullscreen (F11)
  // 2.) The website then enters tab-initiated fullscreen
  // 3.) User performs a press and hold gesture on escape
  //
  // In this case, the fullscreen controller will revert back to browser
  // fullscreen mode but there won't be a fullscreen exit message to trigger
  // the UI cleanup for the exit bubble.  To handle this case, we need to check
  // to make sure the UI is in the right fullscreen mode before proceeding.
  if (!exclusive_access_manager_->fullscreen_controller()
           ->IsWindowFullscreenForTabOrPending()) {
    key_press_delay_timer_.Stop();
    if (IsVisible() && input_entry_method_ == InputEntryMethod::KEYBOARD) {
      Hide(true);
    }
    return;
  }

  // Note: This logic handles the UI feedback element used when holding down the
  // esc key, however the logic for exiting fullscreen is handled by the
  // KeyboardLockController class.
  if (event.type() == ui::EventType::kKeyPressed &&
      !key_press_delay_timer_.IsRunning() &&
      exclusive_access_manager_->keyboard_lock_controller()
          ->RequiresPressAndHoldEscToExit()) {
    key_press_delay_timer_.Start(
        FROM_HERE, kKeyPressPopupDelay,
        base::BindOnce(&FullscreenControlHost::ShowForInputEntryMethod,
                       base::Unretained(this), InputEntryMethod::KEYBOARD));
  } else if (event.type() == ui::EventType::kKeyReleased) {
    key_press_delay_timer_.Stop();
    if (IsVisible() && input_entry_method_ == InputEntryMethod::KEYBOARD) {
      Hide(true);
    }
  }
}

void FullscreenControlHost::OnMouseEvent(const ui::MouseEvent& event) {
  if (!IsExitUiEnabled()) {
    return;
  }

  if (event.type() != ui::EventType::kMouseMoved || IsAnimating() ||
      (input_entry_method_ != InputEntryMethod::NOT_ACTIVE &&
       input_entry_method_ != InputEntryMethod::MOUSE)) {
    return;
  }

  // TODO(crbug.com/957455) Do not show fullscreen exit button while in pointer
  // lock mode. This is only necessary because the current implementation of
  // pointer lock doesn't constrain the mouse cursor position, so the exit
  // button may still appear even though the mouse cursor is invisible and its
  // position is technically undefined. This mitigation will become unnecessary
  // when pointer lock is re-implemented using relative motion events.
  if (IsPointerLocked()) {
    return;
  }

  if (IsExitUiNeeded()) {
    if (IsVisible()) {
      if (event.y() >= CalculateCursorBufferHeight()) {
        Hide(true);
      }
    } else {
      DCHECK_EQ(InputEntryMethod::NOT_ACTIVE, input_entry_method_);
      if (!in_mouse_cooldown_mode_ &&
          event.y() <= kShowFullscreenExitControlHeight) {
        // If the exit fullscreen prompt is being shown (say user just pressed
        // F11 with the cursor on the top of the screen) then we suppress the
        // fullscreen control host and just put it in cooldown mode.
        if (const auto* bubble = browser_view_->GetExclusiveAccessBubble();
            bubble && bubble->IsShowing()) {
          in_mouse_cooldown_mode_ = true;
        } else {
          ShowForInputEntryMethod(InputEntryMethod::MOUSE);
        }
      } else if (in_mouse_cooldown_mode_ &&
                 event.y() >= CalculateCursorBufferHeight()) {
        in_mouse_cooldown_mode_ = false;
      }
    }
  } else if (IsVisible()) {
    Hide(true);
  }
}

void FullscreenControlHost::OnTouchEvent(const ui::TouchEvent& event) {
  if (input_entry_method_ != InputEntryMethod::TOUCH) {
    return;
  }

  DCHECK(IsVisible());

  // Hide the popup if it is showing and the user touches outside of the popup.
  if (event.type() == ui::EventType::kTouchPressed && !IsAnimating()) {
    Hide(true);
  }
}

void FullscreenControlHost::OnGestureEvent(const ui::GestureEvent& event) {
  if (!IsExitUiEnabled()) {
    return;
  }

  if (event.type() == ui::EventType::kGestureLongPress && IsExitUiNeeded() &&
      !IsVisible()) {
    ShowForInputEntryMethod(InputEntryMethod::TOUCH);
  }
}

void FullscreenControlHost::Hide(bool animate) {
  if (IsPopupCreated()) {
    GetPopup()->Hide(animate);
  }
}

bool FullscreenControlHost::IsVisible() const {
  return IsPopupCreated() && fullscreen_control_popup_->IsVisible();
}

void FullscreenControlHost::OnEnterFullscreen() {
  // TODO(crbug.com/439876404): Change this to a CHECK once cause of sequential
  // OnEnterFullscreen() calls is fixed.
  if (event_monitor_) {
    LOG(ERROR) << "FullscreenControlHost: Event monitor already exists";
  }

  if (IsFullscreenExitUIEnabled() && !event_monitor_) {
    event_monitor_ = views::EventMonitor::CreateWindowMonitor(
        this, browser_view_->GetNativeWindow(),
        {ui::EventType::kMouseMoved, ui::EventType::kKeyPressed,
         ui::EventType::kKeyReleased, ui::EventType::kTouchPressed,
         ui::EventType::kGestureLongPress});
  }
}

void FullscreenControlHost::OnExitFullscreen() {
  Hide(false);

  popup_timeout_timer_.Stop();
  key_press_delay_timer_.Stop();

  input_entry_method_ = InputEntryMethod::NOT_ACTIVE;
  in_mouse_cooldown_mode_ = false;
  event_monitor_.reset();
}

FullscreenControlPopup* FullscreenControlHost::GetPopup() {
  if (!IsPopupCreated()) {
    fullscreen_control_popup_ = std::make_unique<FullscreenControlPopup>(
        browser_view_->GetWidget()->GetNativeView(),
        base::BindRepeating(
            &FullscreenControlHost::OnExitFullscreenPopupClicked,
            base::Unretained(this)),
        base::BindRepeating(&FullscreenControlHost::OnVisibilityChanged,
                            base::Unretained(this)));
  }
  return fullscreen_control_popup_.get();
}

bool FullscreenControlHost::IsPopupCreated() const {
  return fullscreen_control_popup_.get() != nullptr;
}

bool FullscreenControlHost::IsAnimating() const {
  return IsPopupCreated() && fullscreen_control_popup_->IsAnimating();
}

void FullscreenControlHost::ShowForInputEntryMethod(
    InputEntryMethod input_entry_method) {
  input_entry_method_ = input_entry_method;
  if (auto* const bubble = browser_view_->GetExclusiveAccessBubble()) {
    bubble->HideImmediately();
  }
  GetPopup()->Show(browser_view_->GetWidget()->GetClientAreaBoundsInScreen());

  // Exit cooldown mode in case the exit UI is triggered by a different method.
  in_mouse_cooldown_mode_ = false;
}

void FullscreenControlHost::OnVisibilityChanged() {
  if (!IsVisible()) {
    input_entry_method_ = InputEntryMethod::NOT_ACTIVE;
    key_press_delay_timer_.Stop();
  } else if (input_entry_method_ == InputEntryMethod::MOUSE) {
    StartPopupTimeout(InputEntryMethod::MOUSE, kMousePopupTimeout);
  } else if (input_entry_method_ == InputEntryMethod::TOUCH) {
    StartPopupTimeout(InputEntryMethod::TOUCH, kTouchPopupTimeout);
  }

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

void FullscreenControlHost::StartPopupTimeout(
    InputEntryMethod expected_input_method,
    base::TimeDelta timeout) {
  popup_timeout_timer_.Start(
      FROM_HERE, timeout,
      base::BindOnce(&FullscreenControlHost::OnPopupTimeout,
                     base::Unretained(this), expected_input_method));
}

void FullscreenControlHost::OnPopupTimeout(
    InputEntryMethod expected_input_method) {
  if (IsVisible() && !IsAnimating() &&
      input_entry_method_ == expected_input_method) {
    if (input_entry_method_ == InputEntryMethod::MOUSE) {
      in_mouse_cooldown_mode_ = true;
    }
    Hide(true);
  }
}

bool FullscreenControlHost::IsExitUiNeeded() {
  return browser_view_->IsFullscreen() &&
         browser_view_->GetExclusiveAccessContext()->CanUserExitFullscreen() &&
         BrowserWindowFullscreenController::From(browser_view_->browser())
             ->ShouldHideUIForFullscreen();
}

bool FullscreenControlHost::IsPointerLocked() {
  if (!browser_view_) {
    return false;
  }

  auto* web_contents = browser_view_->GetActiveWebContents();
  if (!web_contents) {
    return false;
  }

  auto* rwhv = web_contents->GetRenderWidgetHostView();
  if (!rwhv) {
    return false;
  }

  return rwhv->IsPointerLocked();
}

float FullscreenControlHost::CalculateCursorBufferHeight() const {
  float control_bottom = FullscreenControlPopup::GetButtonBottomOffset();
  return control_bottom * kExitHeightScaleFactor;
}

void FullscreenControlHost::OnExitFullscreenPopupClicked() {
  base::RecordAction(
      base::UserMetricsAction("ExitFullscreen_PopupCloseButton"));
  browser_view_->GetExclusiveAccessContext()->ExitFullscreen();
}
