// 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 "content/browser/keyboard_lock/keyboard_lock_service_impl.h"

#include <memory>
#include <optional>
#include <utility>

#include "base/check.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/metrics/histogram_macros.h"
#include "content/browser/keyboard_lock/keyboard_lock_metrics.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/common/content_features.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom.h"
#include "third_party/blink/public/mojom/devtools/console_message.mojom.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/keycode_converter.h"

using blink::mojom::GetKeyboardLayoutMapResult;
using blink::mojom::KeyboardLockRequestResult;

namespace content {

namespace {

void LogKeyboardLockMethodCalled(KeyboardLockMethods method) {
  UMA_HISTOGRAM_ENUMERATION(kKeyboardLockMethodCalledHistogramName, method,
                            KeyboardLockMethods::kCount);
}

}  // namespace

KeyboardLockServiceImpl::KeyboardLockServiceImpl(
    RenderFrameHost& render_frame_host,
    mojo::PendingReceiver<blink::mojom::KeyboardLockService> receiver)
    : DocumentService(render_frame_host, std::move(receiver)) {}

// static
void KeyboardLockServiceImpl::CreateMojoService(
    RenderFrameHost* render_frame_host,
    mojo::PendingReceiver<blink::mojom::KeyboardLockService> receiver) {
  CHECK(render_frame_host);

  // The object is bound to the lifetime of |render_frame_host| and the mojo
  // connection. See DocumentService for details.
  new KeyboardLockServiceImpl(*render_frame_host, std::move(receiver));
}

void KeyboardLockServiceImpl::RequestKeyboardLock(
    const std::vector<std::string>& key_codes,
    RequestKeyboardLockCallback callback) {
  if (key_codes.empty()) {
    LogKeyboardLockMethodCalled(KeyboardLockMethods::kRequestAllKeys);
  } else {
    LogKeyboardLockMethodCalled(KeyboardLockMethods::kRequestSomeKeys);
  }
  if (render_frame_host().GetParentOrOuterDocument()) {
    std::move(callback).Run(KeyboardLockRequestResult::kChildFrameError);
    return;
  }
  if (!render_frame_host().IsActive()) {
    std::move(callback).Run(KeyboardLockRequestResult::kFrameDetachedError);
    return;
  }

  // Per base::flat_set usage notes, the proper way to init a flat_set is
  // inserting into a vector and using that to init the flat_set.
  std::vector<ui::DomCode> dom_codes;
  bool invalid_key_code_found = false;
  for (const std::string& code : key_codes) {
    ui::DomCode dom_code = ui::KeycodeConverter::CodeStringToDomCode(code);
    if (dom_code != ui::DomCode::NONE) {
      dom_codes.push_back(dom_code);
    } else {
      invalid_key_code_found = true;
      render_frame_host().AddMessageToConsole(
          blink::mojom::ConsoleMessageLevel::kWarning,
          "Invalid DOMString passed into keyboard.lock(): '" + code + "'");
    }
  }

  auto& frame_host_impl =
      static_cast<RenderFrameHostImpl&>(render_frame_host());

  // If we are provided with a vector containing one or more invalid key codes,
  // then exit without enabling keyboard lock.  Also cancel any previous
  // keyboard lock request since the most recent request failed.
  if (invalid_key_code_found) {
    frame_host_impl.GetRenderWidgetHost()->CancelKeyboardLock();
    std::move(callback).Run(KeyboardLockRequestResult::kNoValidKeyCodesError);
    return;
  }

  std::optional<base::flat_set<ui::DomCode>> dom_code_set;
  if (!dom_codes.empty()) {
    dom_code_set = std::move(dom_codes);
  }
  frame_host_impl.GetRenderWidgetHost()->RequestKeyboardLock(
      std::move(dom_code_set),
      // We use a lambda function here instead of binding to a method because
      // we want the Mojo callback to be called even if `this` goes out of
      // scope.
      base::BindOnce(
          [](base::WeakPtr<KeyboardLockServiceImpl> weak_this,
             RequestKeyboardLockCallback callback,
             blink::mojom::KeyboardLockRequestResult result) {
            std::move(callback).Run(result);
            if (weak_this &&
                result == blink::mojom::KeyboardLockRequestResult::kSuccess) {
              weak_this->feature_handle_ =
                  static_cast<RenderFrameHostImpl&>(
                      weak_this->render_frame_host())
                      .RegisterBackForwardCacheDisablingNonStickyFeature(
                          blink::scheduler::WebSchedulerTrackedFeature::
                              kKeyboardLock);
            }
          },
          weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
}

void KeyboardLockServiceImpl::CancelKeyboardLock() {
  LogKeyboardLockMethodCalled(KeyboardLockMethods::kCancelLock);
  auto& frame_host_impl =
      static_cast<RenderFrameHostImpl&>(render_frame_host());
  frame_host_impl.GetRenderWidgetHost()->CancelKeyboardLock();
  feature_handle_.Reset();
}

void KeyboardLockServiceImpl::GetKeyboardLayoutMap(
    GetKeyboardLayoutMapCallback callback) {
  auto& frame_host_impl =
      static_cast<RenderFrameHostImpl&>(render_frame_host());

  auto response = GetKeyboardLayoutMapResult::New();
  // The keyboard layout map is only accessible from the outermost main frame or
  // with the permission policy enabled.
  if (frame_host_impl.GetParentOrOuterDocument() &&
      !frame_host_impl.IsFeatureEnabled(
          network::mojom::PermissionsPolicyFeature::kKeyboardMap)) {
    response->status = blink::mojom::GetKeyboardLayoutMapStatus::kDenied;
    std::move(callback).Run(std::move(response));
    return;
  }
  response->status = blink::mojom::GetKeyboardLayoutMapStatus::kSuccess;
  response->layout_map = frame_host_impl.GetPage().GetKeyboardLayoutMap();

  std::move(callback).Run(std::move(response));
}

KeyboardLockServiceImpl::~KeyboardLockServiceImpl() = default;

}  // namespace content
