// Copyright (c) 2012 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 "libcef/browser/frame_host_impl.h"

#include "include/cef_request.h"
#include "include/cef_stream.h"
#include "include/cef_v8.h"
#include "include/test/cef_test_helpers.h"
#include "libcef/browser/browser_host_impl.h"
#include "libcef/browser/navigate_params.h"
#include "libcef/browser/net_service/browser_urlrequest_impl.h"
#include "libcef/common/cef_messages.h"
#include "libcef/common/frame_util.h"
#include "libcef/common/process_message_impl.h"
#include "libcef/common/request_impl.h"
#include "libcef/common/task_runner_impl.h"

#include "content/browser/frame_host/frame_tree_node.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"

namespace {

// Implementation of CommandResponseHandler for calling a CefStringVisitor.
class StringVisitHandler : public CefResponseManager::Handler {
 public:
  explicit StringVisitHandler(CefRefPtr<CefStringVisitor> visitor)
      : visitor_(visitor) {}
  void OnResponse(const Cef_Response_Params& params) override {
    visitor_->Visit(params.response);
  }

 private:
  CefRefPtr<CefStringVisitor> visitor_;

  IMPLEMENT_REFCOUNTING(StringVisitHandler);
};

// Implementation of CommandResponseHandler for calling ViewText().
class ViewTextHandler : public CefResponseManager::Handler {
 public:
  explicit ViewTextHandler(CefRefPtr<CefFrameHostImpl> frame) : frame_(frame) {}
  void OnResponse(const Cef_Response_Params& params) override {
    CefRefPtr<CefBrowser> browser = frame_->GetBrowser();
    if (browser.get()) {
      static_cast<CefBrowserHostImpl*>(browser.get())
          ->ViewText(params.response);
    }
  }

 private:
  CefRefPtr<CefFrameHostImpl> frame_;

  IMPLEMENT_REFCOUNTING(ViewTextHandler);
};

}  // namespace

CefFrameHostImpl::CefFrameHostImpl(scoped_refptr<CefBrowserInfo> browser_info,
                                   bool is_main_frame,
                                   int64_t parent_frame_id)
    : is_main_frame_(is_main_frame),
      frame_id_(kInvalidFrameId),
      browser_info_(browser_info),
      is_focused_(is_main_frame_),  // The main frame always starts focused.
      parent_frame_id_(parent_frame_id) {
#if DCHECK_IS_ON()
  DCHECK(browser_info_);
  if (is_main_frame_) {
    DCHECK_EQ(parent_frame_id_, kInvalidFrameId);
  } else {
    DCHECK_GT(parent_frame_id_, 0);
  }
#endif
}

CefFrameHostImpl::CefFrameHostImpl(scoped_refptr<CefBrowserInfo> browser_info,
                                   content::RenderFrameHost* render_frame_host)
    : is_main_frame_(render_frame_host->GetParent() == nullptr),
      frame_id_(MakeFrameId(render_frame_host)),
      browser_info_(browser_info),
      is_focused_(is_main_frame_),  // The main frame always starts focused.
      url_(render_frame_host->GetLastCommittedURL().spec()),
      name_(render_frame_host->GetFrameName()),
      parent_frame_id_(is_main_frame_
                           ? kInvalidFrameId
                           : MakeFrameId(render_frame_host->GetParent())),
      render_frame_host_(render_frame_host),
      response_manager_(new CefResponseManager) {
  DCHECK(browser_info_);
}

CefFrameHostImpl::~CefFrameHostImpl() {}

void CefFrameHostImpl::SetRenderFrameHost(content::RenderFrameHost* host) {
  CEF_REQUIRE_UIT();

  base::AutoLock lock_scope(state_lock_);

  // We should not be detached.
  CHECK(browser_info_);
  // We should be the main frame.
  CHECK(is_main_frame_);

  render_frame_host_ = host;
  frame_id_ = MakeFrameId(host);
  url_ = host->GetLastCommittedURL().spec();
  name_ = host->GetFrameName();

  // Cancel any existing messages.
  response_manager_.reset(new CefResponseManager);
}

bool CefFrameHostImpl::IsValid() {
  return !!GetBrowserHostImpl();
}

void CefFrameHostImpl::Undo() {
  SendCommand("Undo", nullptr);
}

void CefFrameHostImpl::Redo() {
  SendCommand("Redo", nullptr);
}

void CefFrameHostImpl::Cut() {
  SendCommand("Cut", nullptr);
}

void CefFrameHostImpl::Copy() {
  SendCommand("Copy", nullptr);
}

void CefFrameHostImpl::Paste() {
  SendCommand("Paste", nullptr);
}

void CefFrameHostImpl::Delete() {
  SendCommand("Delete", nullptr);
}

void CefFrameHostImpl::SelectAll() {
  SendCommand("SelectAll", nullptr);
}

void CefFrameHostImpl::ViewSource() {
  SendCommand("GetSource", new ViewTextHandler(this));
}

void CefFrameHostImpl::GetSource(CefRefPtr<CefStringVisitor> visitor) {
  SendCommand("GetSource", new StringVisitHandler(visitor));
}

void CefFrameHostImpl::GetText(CefRefPtr<CefStringVisitor> visitor) {
  SendCommand("GetText", new StringVisitHandler(visitor));
}

void CefFrameHostImpl::LoadRequest(CefRefPtr<CefRequest> request) {
  CefNavigateParams params(GURL(), kPageTransitionExplicit);
  static_cast<CefRequestImpl*>(request.get())->Get(params);
  Navigate(params);
}

void CefFrameHostImpl::LoadURL(const CefString& url) {
  LoadURLWithExtras(url, content::Referrer(), kPageTransitionExplicit,
                    std::string());
}

void CefFrameHostImpl::ExecuteJavaScript(const CefString& jsCode,
                                         const CefString& scriptUrl,
                                         int startLine) {
  SendJavaScript(jsCode, scriptUrl, startLine);
}

bool CefFrameHostImpl::IsMain() {
  return is_main_frame_;
}

bool CefFrameHostImpl::IsFocused() {
  base::AutoLock lock_scope(state_lock_);
  return is_focused_;
}

CefString CefFrameHostImpl::GetName() {
  base::AutoLock lock_scope(state_lock_);
  return name_;
}

int64 CefFrameHostImpl::GetIdentifier() {
  base::AutoLock lock_scope(state_lock_);
  return frame_id_;
}

CefRefPtr<CefFrame> CefFrameHostImpl::GetParent() {
  int64 parent_frame_id;

  {
    base::AutoLock lock_scope(state_lock_);
    if (is_main_frame_ || parent_frame_id_ == kInvalidFrameId)
      return nullptr;
    parent_frame_id = parent_frame_id_;
  }

  auto browser = GetBrowserHostImpl();
  if (browser)
    return browser->GetFrame(parent_frame_id);

  return nullptr;
}

CefString CefFrameHostImpl::GetURL() {
  base::AutoLock lock_scope(state_lock_);
  return url_;
}

CefRefPtr<CefBrowser> CefFrameHostImpl::GetBrowser() {
  return GetBrowserHostImpl().get();
}

CefRefPtr<CefV8Context> CefFrameHostImpl::GetV8Context() {
  NOTREACHED() << "GetV8Context cannot be called from the browser process";
  return nullptr;
}

void CefFrameHostImpl::VisitDOM(CefRefPtr<CefDOMVisitor> visitor) {
  NOTREACHED() << "VisitDOM cannot be called from the browser process";
}

CefRefPtr<CefURLRequest> CefFrameHostImpl::CreateURLRequest(
    CefRefPtr<CefRequest> request,
    CefRefPtr<CefURLRequestClient> client) {
  if (!request || !client)
    return nullptr;

  if (!CefTaskRunnerImpl::GetCurrentTaskRunner()) {
    NOTREACHED() << "called on invalid thread";
    return nullptr;
  }

  auto browser = GetBrowserHostImpl();
  if (!browser)
    return nullptr;

  auto request_context = browser->request_context();

  CefRefPtr<CefBrowserURLRequest> impl =
      new CefBrowserURLRequest(this, request, client, request_context);
  if (impl->Start())
    return impl.get();
  return nullptr;
}

void CefFrameHostImpl::SendProcessMessage(
    CefProcessId target_process,
    CefRefPtr<CefProcessMessage> message) {
  DCHECK_EQ(PID_RENDERER, target_process);
  DCHECK(message.get());

  Cef_Request_Params params;
  CefProcessMessageImpl* impl =
      static_cast<CefProcessMessageImpl*>(message.get());
  if (!impl->CopyTo(params))
    return;

  DCHECK(!params.name.empty());

  params.user_initiated = true;
  params.request_id = -1;
  params.expect_response = false;

  Send(new CefMsg_Request(MSG_ROUTING_NONE, params));
}

void CefFrameHostImpl::SetFocused(bool focused) {
  base::AutoLock lock_scope(state_lock_);
  is_focused_ = focused;
}

void CefFrameHostImpl::RefreshAttributes() {
  CEF_REQUIRE_UIT();

  base::AutoLock lock_scope(state_lock_);
  if (!render_frame_host_)
    return;
  url_ = render_frame_host_->GetLastCommittedURL().spec();

  // Use the assigned name if it is non-empty. This represents the name property
  // on the frame DOM element. If the assigned name is empty, revert to the
  // internal unique name. This matches the logic in render_frame_util::GetName.
  name_ = render_frame_host_->GetFrameName();
  if (name_.empty()) {
    const auto node = content::FrameTreeNode::GloballyFindByID(
        render_frame_host_->GetFrameTreeNodeId());
    if (node) {
      name_ = node->unique_name();
    }
  }

  if (!is_main_frame_)
    parent_frame_id_ = MakeFrameId(render_frame_host_->GetParent());
}

void CefFrameHostImpl::Navigate(const CefNavigateParams& params) {
  CefMsg_LoadRequest_Params request;
  request.url = params.url;
  if (!request.url.is_valid()) {
    LOG(ERROR) << "Invalid URL passed to CefFrameHostImpl::Navigate: "
               << params.url;
    return;
  }

  request.method = params.method;
  request.referrer = params.referrer.url;
  request.referrer_policy =
      CefRequestImpl::BlinkReferrerPolicyToNetReferrerPolicy(
          params.referrer.policy);
  request.site_for_cookies = params.site_for_cookies;
  request.headers = params.headers;
  request.load_flags = params.load_flags;
  request.upload_data = params.upload_data;

  Send(new CefMsg_LoadRequest(MSG_ROUTING_NONE, request));

  auto browser = GetBrowserHostImpl();
  if (browser)
    browser->OnSetFocus(FOCUS_SOURCE_NAVIGATION);
}

void CefFrameHostImpl::LoadURLWithExtras(const std::string& url,
                                         const content::Referrer& referrer,
                                         ui::PageTransition transition,
                                         const std::string& extra_headers) {
  // Only known frame ids or kMainFrameId are supported.
  const auto frame_id = GetFrameId();
  if (frame_id < CefFrameHostImpl::kMainFrameId)
    return;

  if (frame_id == CefFrameHostImpl::kMainFrameId) {
    // Load via the browser using NavigationController.
    auto browser = GetBrowserHostImpl();
    if (browser) {
      browser->LoadMainFrameURL(url, referrer, transition, extra_headers);
    }
  } else {
    CefNavigateParams params(GURL(url), transition);
    params.referrer = referrer;
    params.headers = extra_headers;
    Navigate(params);
  }
}

void CefFrameHostImpl::SendCommand(
    const std::string& command,
    CefRefPtr<CefResponseManager::Handler> responseHandler) {
  DCHECK(!command.empty());

  if (!CEF_CURRENTLY_ON_UIT()) {
    CEF_POST_TASK(CEF_UIT, base::BindOnce(&CefFrameHostImpl::SendCommand, this,
                                          command, responseHandler));
    return;
  }

  // Only known frame ids or kMainFrameId are supported.
  const auto frame_id = GetFrameId();
  if (frame_id < CefFrameHostImpl::kMainFrameId)
    return;

  TRACE_EVENT2("cef", "CefFrameHostImpl::SendCommand", "frame_id", frame_id,
               "needsResponse", responseHandler.get() ? 1 : 0);
  Cef_Request_Params params;
  params.name = "execute-command";
  params.user_initiated = false;

  if (responseHandler.get()) {
    params.request_id = response_manager_->RegisterHandler(responseHandler);
    params.expect_response = true;
  } else {
    params.request_id = -1;
    params.expect_response = false;
  }

  params.arguments.AppendString(command);

  Send(new CefMsg_Request(MSG_ROUTING_NONE, params));
}

void CefFrameHostImpl::SendCode(
    bool is_javascript,
    const std::string& code,
    const std::string& script_url,
    int script_start_line,
    CefRefPtr<CefResponseManager::Handler> responseHandler) {
  DCHECK(!code.empty());
  DCHECK_GE(script_start_line, 0);

  if (!CEF_CURRENTLY_ON_UIT()) {
    CEF_POST_TASK(CEF_UIT, base::BindOnce(&CefFrameHostImpl::SendCode, this,
                                          is_javascript, code, script_url,
                                          script_start_line, responseHandler));
    return;
  }

  // Only known frame ids or kMainFrameId are supported.
  auto frame_id = GetFrameId();
  if (frame_id < CefFrameHostImpl::kMainFrameId)
    return;

  TRACE_EVENT2("cef", "CefFrameHostImpl::SendCommand", "frame_id", frame_id,
               "needsResponse", responseHandler.get() ? 1 : 0);
  Cef_Request_Params params;
  params.name = "execute-code";
  params.user_initiated = false;

  if (responseHandler.get()) {
    params.request_id = response_manager_->RegisterHandler(responseHandler);
    params.expect_response = true;
  } else {
    params.request_id = -1;
    params.expect_response = false;
  }

  params.arguments.AppendBoolean(is_javascript);
  params.arguments.AppendString(code);
  params.arguments.AppendString(script_url);
  params.arguments.AppendInteger(script_start_line);

  Send(new CefMsg_Request(MSG_ROUTING_NONE, params));
}

void CefFrameHostImpl::SendJavaScript(const std::string& jsCode,
                                      const std::string& scriptUrl,
                                      int startLine) {
  if (jsCode.empty())
    return;
  if (startLine <= 0) {
    // A value of 0 is v8::Message::kNoLineNumberInfo in V8. There is code in
    // V8 that will assert on that value (e.g. V8StackTraceImpl::Frame::Frame
    // if a JS exception is thrown) so make sure |startLine| > 0.
    startLine = 1;
  }

  SendCode(true, jsCode, scriptUrl, startLine, nullptr);
}

void CefFrameHostImpl::MaybeSendDidStopLoading() {
  auto rfh = GetRenderFrameHost();
  if (!rfh)
    return;

  // We only want to notify for the highest-level LocalFrame in this frame's
  // renderer process subtree. If this frame has a parent in the same process
  // then the notification will be sent via the parent instead.
  auto rfh_parent = rfh->GetParent();
  if (rfh_parent && rfh_parent->GetProcess() == rfh->GetProcess()) {
    return;
  }

  Send(new CefMsg_DidStopLoading(MSG_ROUTING_NONE));
}

bool CefFrameHostImpl::OnMessageReceived(const IPC::Message& message) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(CefFrameHostImpl, message)
    IPC_MESSAGE_HANDLER(CefHostMsg_FrameAttached, OnAttached)
    IPC_MESSAGE_HANDLER(CefHostMsg_DidFinishLoad, OnDidFinishLoad)
    IPC_MESSAGE_HANDLER(CefHostMsg_UpdateDraggableRegions,
                        OnUpdateDraggableRegions)
    IPC_MESSAGE_HANDLER(CefHostMsg_Request, OnRequest)
    IPC_MESSAGE_HANDLER(CefHostMsg_Response, OnResponse)
    IPC_MESSAGE_HANDLER(CefHostMsg_ResponseAck, OnResponseAck)
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()
  return handled;
}

void CefFrameHostImpl::ExecuteJavaScriptWithUserGestureForTests(
    const CefString& javascript) {
  if (!CEF_CURRENTLY_ON_UIT()) {
    CEF_POST_TASK(
        CEF_UIT,
        base::BindOnce(
            &CefFrameHostImpl::ExecuteJavaScriptWithUserGestureForTests, this,
            javascript));
    return;
  }

  content::RenderFrameHost* rfh = GetRenderFrameHost();
  if (rfh)
    rfh->ExecuteJavaScriptWithUserGestureForTests(javascript);
}

content::RenderFrameHost* CefFrameHostImpl::GetRenderFrameHost() const {
  CEF_REQUIRE_UIT();
  return render_frame_host_;
}

void CefFrameHostImpl::Detach() {
  CEF_REQUIRE_UIT();

  {
    base::AutoLock lock_scope(state_lock_);
    browser_info_ = nullptr;
  }

  // In case we never attached, clean up.
  while (!queued_messages_.empty()) {
    queued_messages_.pop();
  }

  response_manager_.reset();
  render_frame_host_ = nullptr;
}

// static
int64_t CefFrameHostImpl::MakeFrameId(const content::RenderFrameHost* host) {
  CEF_REQUIRE_UIT();
  auto host_nonconst = const_cast<content::RenderFrameHost*>(host);
  return MakeFrameId(host_nonconst->GetProcess()->GetID(),
                     host_nonconst->GetRoutingID());
}

// static
int64_t CefFrameHostImpl::MakeFrameId(int32_t render_process_id,
                                      int32_t render_routing_id) {
  return frame_util::MakeFrameId(render_process_id, render_routing_id);
}

// kMainFrameId must be -1 to align with renderer expectations.
const int64_t CefFrameHostImpl::kMainFrameId = -1;
const int64_t CefFrameHostImpl::kFocusedFrameId = -2;
const int64_t CefFrameHostImpl::kUnspecifiedFrameId = -3;
const int64_t CefFrameHostImpl::kInvalidFrameId = -4;

// This equates to (TT_EXPLICIT | TT_DIRECT_LOAD_FLAG).
const ui::PageTransition CefFrameHostImpl::kPageTransitionExplicit =
    static_cast<ui::PageTransition>(ui::PAGE_TRANSITION_TYPED |
                                    ui::PAGE_TRANSITION_FROM_ADDRESS_BAR);

int64 CefFrameHostImpl::GetFrameId() const {
  base::AutoLock lock_scope(state_lock_);
  return is_main_frame_ ? kMainFrameId : frame_id_;
}

CefRefPtr<CefBrowserHostImpl> CefFrameHostImpl::GetBrowserHostImpl() const {
  base::AutoLock lock_scope(state_lock_);
  if (browser_info_)
    return browser_info_->browser();
  return nullptr;
}

void CefFrameHostImpl::OnAttached() {
  if (!is_attached_) {
    is_attached_ = true;
    while (!queued_messages_.empty()) {
      Send(queued_messages_.front().release());
      queued_messages_.pop();
    }
  }
}

void CefFrameHostImpl::OnDidFinishLoad(const GURL& validated_url,
                                       int http_status_code) {
  auto browser = GetBrowserHostImpl();
  if (browser)
    browser->OnDidFinishLoad(this, validated_url, http_status_code);
}

void CefFrameHostImpl::OnUpdateDraggableRegions(
    const std::vector<Cef_DraggableRegion_Params>& regions) {
  auto browser = GetBrowserHostImpl();
  if (!browser)
    return;

  CefRefPtr<CefDragHandler> handler;
  auto client = browser->GetClient();
  if (client)
    handler = client->GetDragHandler();
  if (!handler)
    return;

  std::vector<CefDraggableRegion> draggable_regions;
  draggable_regions.reserve(regions.size());

  std::vector<Cef_DraggableRegion_Params>::const_iterator it = regions.begin();
  for (; it != regions.end(); ++it) {
    const gfx::Rect& rect(it->bounds);
    const CefRect bounds(rect.x(), rect.y(), rect.width(), rect.height());
    draggable_regions.push_back(CefDraggableRegion(bounds, it->draggable));
  }

  handler->OnDraggableRegionsChanged(browser.get(), this, draggable_regions);
}

void CefFrameHostImpl::OnRequest(const Cef_Request_Params& params) {
  CEF_REQUIRE_UIT();

  bool success = false;
  std::string response;
  bool expect_response_ack = false;

  if (params.user_initiated) {
    auto browser = GetBrowserHostImpl();
    if (browser && browser->client()) {
      // Give the user a chance to handle the request.
      CefRefPtr<CefProcessMessageImpl> message(new CefProcessMessageImpl(
          const_cast<Cef_Request_Params*>(&params), false, true));
      success = browser->client()->OnProcessMessageReceived(
          browser.get(), this, PID_RENDERER, message.get());
      message->Detach(nullptr);
    }
  } else {
    // Invalid request.
    NOTREACHED();
  }

  if (params.expect_response) {
    DCHECK_GE(params.request_id, 0);

    // Send a response to the renderer.
    Cef_Response_Params response_params;
    response_params.request_id = params.request_id;
    response_params.success = success;
    response_params.response = response;
    response_params.expect_response_ack = expect_response_ack;
    Send(new CefMsg_Response(MSG_ROUTING_NONE, response_params));
  }
}

void CefFrameHostImpl::OnResponse(const Cef_Response_Params& params) {
  CEF_REQUIRE_UIT();

  response_manager_->RunHandler(params);
  if (params.expect_response_ack)
    Send(new CefMsg_ResponseAck(MSG_ROUTING_NONE, params.request_id));
}

void CefFrameHostImpl::OnResponseAck(int request_id) {
  response_manager_->RunAckHandler(request_id);
}

void CefFrameHostImpl::Send(IPC::Message* message) {
  if (!CEF_CURRENTLY_ON_UIT()) {
    CEF_POST_TASK(CEF_UIT,
                  base::BindOnce(base::IgnoreResult(&CefFrameHostImpl::Send),
                                 this, message));
    return;
  }

  if (!render_frame_host_) {
    // Either we're a placeholder frame without a renderer representation, or
    // we've been detached.
    delete message;
    return;
  }

  if (!is_attached_) {
    // Queue messages until we're notified by the renderer that it's ready to
    // handle them.
    queued_messages_.push(base::WrapUnique(message));
    return;
  }

  message->set_routing_id(render_frame_host_->GetRoutingID());
  render_frame_host_->Send(message);
}

void CefExecuteJavaScriptWithUserGestureForTests(CefRefPtr<CefFrame> frame,
                                                 const CefString& javascript) {
  CefFrameHostImpl* impl = static_cast<CefFrameHostImpl*>(frame.get());
  if (impl)
    impl->ExecuteJavaScriptWithUserGestureForTests(javascript);
}