// Copyright (c) 2012 The Chromium 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 "content/renderer/devtools/devtools_agent.h"

#include <stddef.h>

#include <map>

#include "base/json/json_writer.h"
#include "base/lazy_instance.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/threading/non_thread_safe.h"
#include "base/trace_event/trace_event.h"
#include "content/common/devtools_messages.h"
#include "content/common/frame_messages.h"
#include "content/public/common/manifest.h"
#include "content/renderer/devtools/devtools_client.h"
#include "content/renderer/devtools/devtools_cpu_throttler.h"
#include "content/renderer/manifest/manifest_manager.h"
#include "content/renderer/render_frame_impl.h"
#include "content/renderer/render_widget.h"
#include "ipc/ipc_channel.h"
#include "third_party/WebKit/public/platform/WebFloatRect.h"
#include "third_party/WebKit/public/platform/WebPoint.h"
#include "third_party/WebKit/public/platform/WebString.h"
#include "third_party/WebKit/public/web/WebDevToolsAgent.h"
#include "third_party/WebKit/public/web/WebLocalFrame.h"

using blink::WebDevToolsAgent;
using blink::WebDevToolsAgentClient;
using blink::WebLocalFrame;
using blink::WebPoint;
using blink::WebString;

using base::trace_event::TraceLog;

namespace content {

namespace {

const size_t kMaxMessageChunkSize = IPC::Channel::kMaximumMessageSize / 4;
const char kPageGetAppManifest[] = "Page.getAppManifest";

class WebKitClientMessageLoopImpl
    : public WebDevToolsAgentClient::WebKitClientMessageLoop,
      public base::NonThreadSafe {
 public:
  WebKitClientMessageLoopImpl() = default;
  ~WebKitClientMessageLoopImpl() override { DCHECK(CalledOnValidThread()); }
  void run() override {
    DCHECK(CalledOnValidThread());

    base::RunLoop* const previous_run_loop = run_loop_;
    base::RunLoop run_loop;
    run_loop_ = &run_loop;

    base::MessageLoop::ScopedNestableTaskAllower allow(
        base::MessageLoop::current());
    run_loop.Run();

    run_loop_ = previous_run_loop;
  }
  void quitNow() override {
    DCHECK(CalledOnValidThread());
    DCHECK(run_loop_);

    run_loop_->Quit();
  }

 private:
  base::RunLoop* run_loop_ = nullptr;
};

typedef std::map<int, DevToolsAgent*> IdToAgentMap;
base::LazyInstance<IdToAgentMap>::Leaky
    g_agent_for_routing_id = LAZY_INSTANCE_INITIALIZER;

} //  namespace

DevToolsAgent::DevToolsAgent(RenderFrameImpl* frame)
    : RenderFrameObserver(frame),
      is_attached_(false),
      is_devtools_client_(false),
      paused_(false),
      frame_(frame),
      cpu_throttler_(new DevToolsCPUThrottler()),
      weak_factory_(this) {
  g_agent_for_routing_id.Get()[routing_id()] = this;
  frame_->GetWebFrame()->setDevToolsAgentClient(this);
}

DevToolsAgent::~DevToolsAgent() {
  g_agent_for_routing_id.Get().erase(routing_id());
}

// Called on the Renderer thread.
bool DevToolsAgent::OnMessageReceived(const IPC::Message& message) {
  bool handled = true;
  IPC_BEGIN_MESSAGE_MAP(DevToolsAgent, message)
    IPC_MESSAGE_HANDLER(DevToolsAgentMsg_Attach, OnAttach)
    IPC_MESSAGE_HANDLER(DevToolsAgentMsg_Reattach, OnReattach)
    IPC_MESSAGE_HANDLER(DevToolsAgentMsg_Detach, OnDetach)
    IPC_MESSAGE_HANDLER(DevToolsAgentMsg_DispatchOnInspectorBackend,
                        OnDispatchOnInspectorBackend)
    IPC_MESSAGE_HANDLER(DevToolsAgentMsg_InspectElement, OnInspectElement)
    IPC_MESSAGE_HANDLER(DevToolsAgentMsg_RequestNewWindow_ACK,
                        OnRequestNewWindowACK)
    IPC_MESSAGE_HANDLER(DevToolsMsg_SetupDevToolsClient, OnSetupDevToolsClient)
    IPC_MESSAGE_UNHANDLED(handled = false)
  IPC_END_MESSAGE_MAP()

  if (message.type() == FrameMsg_Navigate::ID)
    ContinueProgram();  // Don't want to swallow the message.

  return handled;
}

void DevToolsAgent::WidgetWillClose() {
  ContinueProgram();
}

void DevToolsAgent::OnDestruct() {
  delete this;
}

void DevToolsAgent::sendProtocolMessage(int session_id,
                                        int call_id,
                                        const blink::WebString& message,
                                        const blink::WebString& state_cookie) {
  if (!send_protocol_message_callback_for_test_.is_null()) {
    send_protocol_message_callback_for_test_.Run(
        session_id, call_id, message.utf8(), state_cookie.utf8());
    return;
  }
  SendChunkedProtocolMessage(this, routing_id(), session_id, call_id,
                             message.utf8(), state_cookie.utf8());
}

// static
blink::WebDevToolsAgentClient::WebKitClientMessageLoop*
DevToolsAgent::createMessageLoopWrapper() {
  return new WebKitClientMessageLoopImpl();
}

blink::WebDevToolsAgentClient::WebKitClientMessageLoop*
DevToolsAgent::createClientMessageLoop() {
  return createMessageLoopWrapper();
}

void DevToolsAgent::willEnterDebugLoop() {
  paused_ = true;
}

void DevToolsAgent::didExitDebugLoop() {
  paused_ = false;
}

bool DevToolsAgent::requestDevToolsForFrame(blink::WebLocalFrame* webFrame) {
  RenderFrameImpl* frame = RenderFrameImpl::FromWebFrame(webFrame);
  if (!frame)
    return false;
  Send(new DevToolsAgentHostMsg_RequestNewWindow(routing_id(),
      frame->GetRoutingID()));
  return true;
}

void DevToolsAgent::enableTracing(const WebString& category_filter) {
  // Tracing is already started by DevTools TracingHandler::Start for the
  // renderer target in the browser process. It will eventually start tracing in
  // the renderer process via IPC. But we still need a redundant
  // TraceLog::SetEnabled call here for
  // InspectorTracingAgent::emitMetadataEvents(), at which point, we are not
  // sure if tracing is already started in the renderer process.
  TraceLog* trace_log = TraceLog::GetInstance();
  trace_log->SetEnabled(
      base::trace_event::TraceConfig(category_filter.utf8(), ""),
      TraceLog::RECORDING_MODE);
}

void DevToolsAgent::disableTracing() {
  TraceLog::GetInstance()->SetDisabled();
}

void DevToolsAgent::setCPUThrottlingRate(double rate) {
  cpu_throttler_->SetThrottlingRate(rate);
}

// static
DevToolsAgent* DevToolsAgent::FromRoutingId(int routing_id) {
  IdToAgentMap::iterator it = g_agent_for_routing_id.Get().find(routing_id);
  if (it != g_agent_for_routing_id.Get().end()) {
    return it->second;
  }
  return NULL;
}

// static
void DevToolsAgent::SendChunkedProtocolMessage(IPC::Sender* sender,
                                               int routing_id,
                                               int session_id,
                                               int call_id,
                                               const std::string& message,
                                               const std::string& post_state) {
  DevToolsMessageChunk chunk;
  chunk.message_size = message.size();
  chunk.is_first = true;

  if (message.length() < kMaxMessageChunkSize) {
    chunk.data = message;
    chunk.session_id = session_id;
    chunk.call_id = call_id;
    chunk.post_state = post_state;
    chunk.is_last = true;
    sender->Send(new DevToolsClientMsg_DispatchOnInspectorFrontend(
                     routing_id, chunk));
    return;
  }

  for (size_t pos = 0; pos < message.length(); pos += kMaxMessageChunkSize) {
    chunk.is_last = pos + kMaxMessageChunkSize >= message.length();
    chunk.session_id = chunk.is_last ? session_id : 0;
    chunk.call_id = chunk.is_last ? call_id : 0;
    chunk.post_state = chunk.is_last ? post_state : std::string();
    chunk.data = message.substr(pos, kMaxMessageChunkSize);
    sender->Send(new DevToolsClientMsg_DispatchOnInspectorFrontend(
                     routing_id, chunk));
    chunk.is_first = false;
    chunk.message_size = 0;
  }
}

void DevToolsAgent::OnAttach(const std::string& host_id, int session_id) {
  GetWebAgent()->attach(WebString::fromUTF8(host_id), session_id);
  is_attached_ = true;
}

void DevToolsAgent::OnReattach(const std::string& host_id,
                               int session_id,
                               const std::string& agent_state) {
  GetWebAgent()->reattach(WebString::fromUTF8(host_id), session_id,
                          WebString::fromUTF8(agent_state));
  is_attached_ = true;
}

void DevToolsAgent::OnDetach() {
  GetWebAgent()->detach();
  is_attached_ = false;
}

void DevToolsAgent::OnDispatchOnInspectorBackend(int session_id,
                                                 int call_id,
                                                 const std::string& method,
                                                 const std::string& message) {
  TRACE_EVENT0("devtools", "DevToolsAgent::OnDispatchOnInspectorBackend");
  if (method == kPageGetAppManifest) {
    ManifestManager* manager = frame_->manifest_manager();
    manager->GetManifest(
        base::Bind(&DevToolsAgent::GotManifest,
        weak_factory_.GetWeakPtr(), session_id, call_id));
    return;
  }
  GetWebAgent()->dispatchOnInspectorBackend(session_id,
                                            call_id,
                                            WebString::fromUTF8(method),
                                            WebString::fromUTF8(message));
}

void DevToolsAgent::OnInspectElement(int session_id, int x, int y) {
  blink::WebFloatRect point_rect(x, y, 0, 0);
  frame_->GetRenderWidget()->convertWindowToViewport(&point_rect);
  GetWebAgent()->inspectElementAt(
      session_id, WebPoint(point_rect.x, point_rect.y));
}

void DevToolsAgent::OnRequestNewWindowACK(bool success) {
  if (!success)
    GetWebAgent()->failedToRequestDevTools();
}

void DevToolsAgent::ContinueProgram() {
  GetWebAgent()->continueProgram();
}

void DevToolsAgent::OnSetupDevToolsClient(
    const std::string& compatibility_script) {
  // We only want to register once; and only in main frame.
  DCHECK(!frame_->GetWebFrame()->parent());
  if (is_devtools_client_)
    return;
  is_devtools_client_ = true;
  new DevToolsClient(frame_, compatibility_script);
}

WebDevToolsAgent* DevToolsAgent::GetWebAgent() {
  return frame_->GetWebFrame()->devToolsAgent();
}

bool DevToolsAgent::IsAttached() {
  return is_attached_;
}

void DevToolsAgent::GotManifest(int session_id,
                                int call_id,
                                const GURL& manifest_url,
                                const Manifest& manifest,
                                const ManifestDebugInfo& debug_info) {
  if (!is_attached_)
    return;

  std::unique_ptr<base::DictionaryValue> response(new base::DictionaryValue());
  response->SetInteger("id", call_id);
  std::unique_ptr<base::DictionaryValue> result(new base::DictionaryValue());
  std::unique_ptr<base::ListValue> errors(new base::ListValue());

  bool failed = false;
  for (const auto& error : debug_info.errors) {
    std::unique_ptr<base::DictionaryValue> error_value(
        new base::DictionaryValue());
    error_value->SetString("message", error.message);
    error_value->SetBoolean("critical", error.critical);
    error_value->SetInteger("line", error.line);
    error_value->SetInteger("column", error.column);
    if (error.critical)
      failed = true;
    errors->Append(std::move(error_value));
  }

  WebString url = frame_->GetWebFrame()->document().manifestURL().string();
  result->SetString("url", url);
  if (!failed)
    result->SetString("data", debug_info.raw_data);
  result->Set("errors", errors.release());
  response->Set("result", result.release());

  std::string json_message;
  base::JSONWriter::Write(*response, &json_message);
  SendChunkedProtocolMessage(this, routing_id(), session_id, call_id,
                             json_message, std::string());
}

}  // namespace content
