// Copyright (c) 2022 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

#include "tests/cefclient/renderer/ipc_performance_test.h"

#include "include/cef_shared_process_message_builder.h"
#include "include/wrapper/cef_helpers.h"
#include "tests/shared/common/binary_value_utils.h"

namespace {

// JS object member names.
constexpr char kMessageSize[] = "size";
constexpr char kTestId[] = "testId";
constexpr CefV8Value::PropertyAttribute kAttributes =
    static_cast<CefV8Value::PropertyAttribute>(
        V8_PROPERTY_ATTRIBUTE_READONLY | V8_PROPERTY_ATTRIBUTE_DONTENUM |
        V8_PROPERTY_ATTRIBUTE_DONTDELETE);

struct TestInfo {
  size_t message_size = 0;
  int id = 0;
  bool is_valid = false;
};

TestInfo GetTest(const CefV8ValueList& arguments, CefString& exception) {
  TestInfo info{};

  if (arguments.size() != 1 || !arguments[0]->IsObject()) {
    exception = "Invalid arguments; expecting a single object";
    return info;
  }

  CefRefPtr<CefV8Value> arg = arguments[0];
  CefRefPtr<CefV8Value> message_size = arg->GetValue(kMessageSize);
  if (!message_size.get() || !message_size->IsInt()) {
    exception =
        "Invalid arguments; object member 'size' is required and must have "
        "integer type";
    return info;
  }

  if (message_size->GetIntValue() < 1) {
    exception =
        "Invalid arguments; object member 'size' must be "
        "positive";
    return info;
  }

  CefRefPtr<CefV8Value> test_id = arg->GetValue(kTestId);
  if (!message_size.get() || !message_size->IsInt()) {
    exception =
        "Invalid arguments; object member 'testId' is required and must "
        "have integer type";
    return info;
  }

  info.message_size = static_cast<size_t>(message_size->GetIntValue());
  info.id = test_id->GetIntValue();
  info.is_valid = true;

  return info;
}

// Handle bindings in the render process.
class IpcDelegate final : public client::ClientAppRenderer::Delegate {
 public:
  class V8HandlerImpl final : public CefV8Handler {
   public:
    explicit V8HandlerImpl(const CefRefPtr<IpcDelegate>& delegate)
        : delegate_(delegate) {}
    V8HandlerImpl(const V8HandlerImpl&) = delete;
    V8HandlerImpl& operator=(const V8HandlerImpl&) = delete;

    bool Execute(const CefString& name,
                 CefRefPtr<CefV8Value> object,
                 const CefV8ValueList& arguments,
                 CefRefPtr<CefV8Value>& retval,
                 CefString& exception) override {
      if (name == bv_utils::kTestSendProcessMessage) {
        const auto test = GetTest(arguments, exception);
        if (test.is_valid) {
          SendTestProcessMessage(test.message_size, test.id);
        }
        return true;
      }

      if (name == bv_utils::kTestSendSMRProcessMessage) {
        const auto test = GetTest(arguments, exception);
        if (test.is_valid) {
          SendTestSMRProcessMessage(test.message_size, test.id);
        }
        return true;
      }

      return false;
    }

   private:
    void SendTestProcessMessage(size_t message_size, int test_id) {
      auto context = CefV8Context::GetCurrentContext();
      delegate_->SendTestProcessMessage(context->GetFrame(), message_size,
                                        test_id);
    }

    void SendTestSMRProcessMessage(size_t message_size, int test_id) {
      auto context = CefV8Context::GetCurrentContext();
      delegate_->SendTestSMRProcessMessage(context->GetFrame(), message_size,
                                           test_id);
    }

    CefRefPtr<IpcDelegate> delegate_;
    IMPLEMENT_REFCOUNTING(V8HandlerImpl);
  };

  IpcDelegate() = default;
  IpcDelegate(const IpcDelegate&) = delete;
  IpcDelegate& operator=(const IpcDelegate&) = delete;

  void OnContextCreated(CefRefPtr<client::ClientAppRenderer> app,
                        CefRefPtr<CefBrowser> browser,
                        CefRefPtr<CefFrame> frame,
                        CefRefPtr<CefV8Context> context) override {
    CEF_REQUIRE_RENDERER_THREAD();

    CefRefPtr<CefV8Handler> handler = new V8HandlerImpl(this);

    // Register function handlers with the 'window' object.
    auto window = context->GetGlobal();
    window->SetValue(
        bv_utils::kTestSendProcessMessage,
        CefV8Value::CreateFunction(bv_utils::kTestSendProcessMessage, handler),
        kAttributes);

    window->SetValue(bv_utils::kTestSendSMRProcessMessage,
                     CefV8Value::CreateFunction(
                         bv_utils::kTestSendSMRProcessMessage, handler),
                     kAttributes);
  }

  bool OnProcessMessageReceived(CefRefPtr<client::ClientAppRenderer> app,
                                CefRefPtr<CefBrowser> browser,
                                CefRefPtr<CefFrame> frame,
                                CefProcessId source_process,
                                CefRefPtr<CefProcessMessage> message) override {
    CEF_REQUIRE_RENDERER_THREAD();
    const auto finish_time = bv_utils::Now();

    if (message->GetName() == bv_utils::kTestSendProcessMessage) {
      auto args = message->GetArgumentList();
      DCHECK_EQ(args->GetSize(), 1U);

      const auto browser_msg =
          bv_utils::GetBrowserMsgFromBinary(args->GetBinary(0));
      PassTestResultToJs(frame, finish_time, browser_msg);

      return true;
    }

    if (message->GetName() == bv_utils::kTestSendSMRProcessMessage) {
      const auto region = message->GetSharedMemoryRegion();
      DCHECK(region->IsValid());
      DCHECK_GE(region->Size(), sizeof(bv_utils::BrowserMessage));

      const auto browser_msg =
          static_cast<const bv_utils::BrowserMessage*>(region->Memory());

      PassTestResultToJs(frame, finish_time, *browser_msg);

      return true;
    }

    return false;
  }

 private:
  IMPLEMENT_REFCOUNTING(IpcDelegate);

  void SendTestProcessMessage(CefRefPtr<CefFrame> frame,
                              size_t message_size,
                              int test_id) {
    CEF_REQUIRE_RENDERER_THREAD();

    auto process_message =
        CefProcessMessage::Create(bv_utils::kTestSendProcessMessage);
    auto args = process_message->GetArgumentList();

    const auto buffer_size =
        std::max(message_size, sizeof(bv_utils::RendererMessage));
    std::vector<uint8_t> buffer(buffer_size);

    const auto renderer_msg =
        reinterpret_cast<bv_utils::RendererMessage*>(buffer.data());

    renderer_msg->test_id = test_id;
    renderer_msg->start_time = bv_utils::Now();

    args->SetBinary(0, bv_utils::CreateCefBinaryValue(buffer));
    frame->SendProcessMessage(PID_BROWSER, process_message);
  }

  void SendTestSMRProcessMessage(CefRefPtr<CefFrame> frame,
                                 size_t message_size,
                                 int test_id) {
    CEF_REQUIRE_RENDERER_THREAD();

    const auto buffer_size =
        std::max(message_size, sizeof(bv_utils::RendererMessage));

    std::vector<uint8_t> buffer(buffer_size);
    const auto renderer_msg =
        reinterpret_cast<bv_utils::RendererMessage*>(buffer.data());
    renderer_msg->test_id = test_id;
    renderer_msg->start_time = bv_utils::Now();

    auto builder = CefSharedProcessMessageBuilder::Create(
        bv_utils::kTestSendSMRProcessMessage, buffer_size);

    bv_utils::CopyDataIntoMemory(buffer, builder->Memory());

    frame->SendProcessMessage(PID_BROWSER, builder->Build());
  }

  // Execute the onSuccess JavaScript callback.
  void PassTestResultToJs(CefRefPtr<CefFrame> frame,
                          const bv_utils::TimePoint& finish_time,
                          const bv_utils::BrowserMessage& msg) {
    const auto rendered_to_browser = msg.duration;
    const auto browser_to_rendered = finish_time - msg.start_time;

    CefString code = "testSendProcessMessageResult(" +
                     std::to_string(msg.test_id) + ", " +
                     bv_utils::ToMicroSecString(rendered_to_browser) + ", " +
                     bv_utils::ToMicroSecString(browser_to_rendered) + ");";

    frame->ExecuteJavaScript(code, frame->GetURL(), 0);
  }
};

}  // namespace

namespace client::ipc_performance_test {

void CreateDelegates(ClientAppRenderer::DelegateSet& delegates) {
  delegates.insert(new IpcDelegate());
}

}  // namespace client::ipc_performance_test
