// Copyright (c) 2011 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 "chrome/browser/debugger/devtools_manager.h"

#include <vector>

#include "base/auto_reset.h"
#include "base/message_loop.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/debugger/devtools_client_host.h"
#include "chrome/browser/debugger/devtools_netlog_observer.h"
#include "chrome/browser/debugger/devtools_window.h"
#include "chrome/browser/io_thread.h"
#include "chrome/browser/prefs/pref_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h"
#include "chrome/common/devtools_messages.h"
#include "chrome/common/pref_names.h"
#include "content/browser/browsing_instance.h"
#include "content/browser/child_process_security_policy.h"
#include "content/browser/renderer_host/render_view_host.h"
#include "content/browser/site_instance.h"
#include "content/common/notification_service.h"
#include "googleurl/src/gurl.h"

// static
DevToolsManager* DevToolsManager::GetInstance() {
  // http://crbug.com/47806 this method may be called when BrowserProcess
  // has already been destroyed.
  if (!g_browser_process)
    return NULL;
  return g_browser_process->devtools_manager();
}

// static
void DevToolsManager::RegisterUserPrefs(PrefService* prefs) {
  prefs->RegisterBooleanPref(prefs::kDevToolsOpenDocked, true);
}

DevToolsManager::DevToolsManager()
    : inspected_rvh_for_reopen_(NULL),
      in_initial_show_(false),
      last_orphan_cookie_(0) {
  registrar_.Add(this, NotificationType::RENDER_VIEW_HOST_DELETED,
                 NotificationService::AllSources());
}

DevToolsManager::~DevToolsManager() {
  DCHECK(inspected_rvh_to_client_host_.empty());
  DCHECK(client_host_to_inspected_rvh_.empty());
  // By the time we destroy devtools manager, all orphan client hosts should
  // have been delelted, no need to notify them upon tab closing.
  DCHECK(orphan_client_hosts_.empty());
}

DevToolsClientHost* DevToolsManager::GetDevToolsClientHostFor(
    RenderViewHost* inspected_rvh) {
  InspectedRvhToClientHostMap::iterator it =
      inspected_rvh_to_client_host_.find(inspected_rvh);
  if (it != inspected_rvh_to_client_host_.end())
    return it->second;
  return NULL;
}

void DevToolsManager::RegisterDevToolsClientHostFor(
    RenderViewHost* inspected_rvh,
    DevToolsClientHost* client_host) {
  DCHECK(!GetDevToolsClientHostFor(inspected_rvh));

  DevToolsRuntimeProperties initial_properties;
  BindClientHost(inspected_rvh, client_host, initial_properties);
  client_host->set_close_listener(this);
  SendAttachToAgent(inspected_rvh);
}

void DevToolsManager::ForwardToDevToolsAgent(
    RenderViewHost* client_rvh,
    const IPC::Message& message) {
  DevToolsClientHost* client_host = FindOwnerDevToolsClientHost(client_rvh);
  if (client_host)
    ForwardToDevToolsAgent(client_host, message);
}

void DevToolsManager::ForwardToDevToolsAgent(DevToolsClientHost* from,
                                             const IPC::Message& message) {
  RenderViewHost* inspected_rvh = GetInspectedRenderViewHost(from);
  if (!inspected_rvh) {
    // TODO(yurys): notify client that the agent is no longer available
    NOTREACHED();
    return;
  }

  IPC::Message* m = new IPC::Message(message);
  m->set_routing_id(inspected_rvh->routing_id());
  inspected_rvh->Send(m);
}

void DevToolsManager::ForwardToDevToolsClient(RenderViewHost* inspected_rvh,
                                              const IPC::Message& message) {
  DevToolsClientHost* client_host = GetDevToolsClientHostFor(inspected_rvh);
  if (!client_host) {
    // Client window was closed while there were messages
    // being sent to it.
    return;
  }
  client_host->SendMessageToClient(message);
}

void DevToolsManager::ActivateWindow(RenderViewHost* client_rvh) {
  DevToolsClientHost* client_host = FindOwnerDevToolsClientHost(client_rvh);
  if (!client_host)
    return;

  DevToolsWindow* window = client_host->AsDevToolsWindow();
  DCHECK(window);
  window->Activate();
}

void DevToolsManager::CloseWindow(RenderViewHost* client_rvh) {
  DevToolsClientHost* client_host = FindOwnerDevToolsClientHost(client_rvh);
  if (client_host) {
    RenderViewHost* inspected_rvh = GetInspectedRenderViewHost(client_host);
    DCHECK(inspected_rvh);
    UnregisterDevToolsClientHostFor(inspected_rvh);
  }
}

void DevToolsManager::RequestDockWindow(RenderViewHost* client_rvh) {
  ReopenWindow(client_rvh, true);
}

void DevToolsManager::RequestUndockWindow(RenderViewHost* client_rvh) {
  ReopenWindow(client_rvh, false);
}

void DevToolsManager::OpenDevToolsWindow(RenderViewHost* inspected_rvh) {
  ToggleDevToolsWindow(
      inspected_rvh,
      true,
      DEVTOOLS_TOGGLE_ACTION_NONE);
}

void DevToolsManager::ToggleDevToolsWindow(
    RenderViewHost* inspected_rvh,
    DevToolsToggleAction action) {
  ToggleDevToolsWindow(inspected_rvh, false, action);
}

void DevToolsManager::RuntimePropertyChanged(RenderViewHost* inspected_rvh,
                                             const std::string& name,
                                             const std::string& value) {
  RuntimePropertiesMap::iterator it =
      runtime_properties_map_.find(inspected_rvh);
  if (it == runtime_properties_map_.end()) {
    std::pair<RenderViewHost*, DevToolsRuntimeProperties> value(
        inspected_rvh,
        DevToolsRuntimeProperties());
    it = runtime_properties_map_.insert(value).first;
  }
  it->second[name] = value;
}

void DevToolsManager::InspectElement(RenderViewHost* inspected_rvh,
                                     int x,
                                     int y) {
  IPC::Message* m = new DevToolsAgentMsg_InspectElement(x, y);
  m->set_routing_id(inspected_rvh->routing_id());
  inspected_rvh->Send(m);
  // TODO(loislo): we should initiate DevTools window opening from within
  // renderer. Otherwise, we still can hit a race condition here.
  OpenDevToolsWindow(inspected_rvh);
}

void DevToolsManager::ClientHostClosing(DevToolsClientHost* host) {
  RenderViewHost* inspected_rvh = GetInspectedRenderViewHost(host);
  if (!inspected_rvh) {
    // It might be in the list of orphan client hosts, remove it from there.
    for (OrphanClientHosts::iterator it = orphan_client_hosts_.begin();
         it != orphan_client_hosts_.end(); ++it) {
      if (it->second.first == host) {
        orphan_client_hosts_.erase(it->first);
        return;
      }
    }
    return;
  }

  NotificationService::current()->Notify(
      NotificationType::DEVTOOLS_WINDOW_CLOSING,
      Source<Profile>(inspected_rvh->site_instance()->GetProcess()->profile()),
      Details<RenderViewHost>(inspected_rvh));

  UnbindClientHost(inspected_rvh, host);
}

void DevToolsManager::Observe(NotificationType type,
                              const NotificationSource& source,
                              const NotificationDetails& details) {
  DCHECK(type == NotificationType::RENDER_VIEW_HOST_DELETED);
  UnregisterDevToolsClientHostFor(Source<RenderViewHost>(source).ptr());
}

RenderViewHost* DevToolsManager::GetInspectedRenderViewHost(
    DevToolsClientHost* client_host) {
  ClientHostToInspectedRvhMap::iterator it =
      client_host_to_inspected_rvh_.find(client_host);
  if (it != client_host_to_inspected_rvh_.end())
    return it->second;
  return NULL;
}

void DevToolsManager::UnregisterDevToolsClientHostFor(
      RenderViewHost* inspected_rvh) {
  DevToolsClientHost* host = GetDevToolsClientHostFor(inspected_rvh);
  if (!host)
    return;
  UnbindClientHost(inspected_rvh, host);
  host->InspectedTabClosing();
}

void DevToolsManager::OnNavigatingToPendingEntry(RenderViewHost* rvh,
                                                 RenderViewHost* dest_rvh,
                                                 const GURL& gurl) {
  if (in_initial_show_) {
    // Mute this even in case it is caused by the initial show routines.
    return;
  }

  int cookie = DetachClientHost(rvh);
  if (cookie != -1) {
    // Navigating to URL in the inspected window.
    AttachClientHost(cookie, dest_rvh);

    DevToolsClientHost* client_host = GetDevToolsClientHostFor(dest_rvh);
    client_host->FrameNavigating(gurl.spec());

    return;
  }

  // Iterate over client hosts and if there is one that has render view host
  // changing, reopen entire client window (this must be caused by the user
  // manually refreshing its content).
  for (ClientHostToInspectedRvhMap::iterator it =
           client_host_to_inspected_rvh_.begin();
       it != client_host_to_inspected_rvh_.end(); ++it) {
    DevToolsWindow* window = it->first->AsDevToolsWindow();
    if (window && window->GetRenderViewHost() == rvh) {
      inspected_rvh_for_reopen_ = it->second;
      MessageLoop::current()->PostTask(FROM_HERE,
          NewRunnableMethod(this,
                            &DevToolsManager::ForceReopenWindow));
      return;
    }
  }
}

void DevToolsManager::TabReplaced(TabContentsWrapper* old_tab,
                                  TabContentsWrapper* new_tab) {
  RenderViewHost* old_rvh = old_tab->tab_contents()->render_view_host();
  DevToolsClientHost* client_host = GetDevToolsClientHostFor(old_rvh);
  if (!client_host)
    return;  // Didn't know about old_tab.
  int cookie = DetachClientHost(old_rvh);
  if (cookie == -1)
    return;  // Didn't know about old_tab.

  client_host->TabReplaced(new_tab);
  AttachClientHost(cookie, new_tab->tab_contents()->render_view_host());
}

int DevToolsManager::DetachClientHost(RenderViewHost* from_rvh) {
  DevToolsClientHost* client_host = GetDevToolsClientHostFor(from_rvh);
  if (!client_host)
    return -1;

  int cookie = last_orphan_cookie_++;
  orphan_client_hosts_[cookie] =
      std::pair<DevToolsClientHost*, DevToolsRuntimeProperties>(
          client_host, runtime_properties_map_[from_rvh]);

  UnbindClientHost(from_rvh, client_host);
  return cookie;
}

void DevToolsManager::AttachClientHost(int client_host_cookie,
                                       RenderViewHost* to_rvh) {
  OrphanClientHosts::iterator it = orphan_client_hosts_.find(
      client_host_cookie);
  if (it == orphan_client_hosts_.end())
    return;

  DevToolsClientHost* client_host = (*it).second.first;
  BindClientHost(to_rvh, client_host, (*it).second.second);
  SendAttachToAgent(to_rvh);

  orphan_client_hosts_.erase(client_host_cookie);
}

void DevToolsManager::SendAttachToAgent(RenderViewHost* inspected_rvh) {
  if (inspected_rvh) {
    ChildProcessSecurityPolicy::GetInstance()->GrantReadRawCookies(
        inspected_rvh->process()->id());

    DevToolsRuntimeProperties properties;
    RuntimePropertiesMap::iterator it =
        runtime_properties_map_.find(inspected_rvh);
    if (it != runtime_properties_map_.end()) {
      properties = DevToolsRuntimeProperties(it->second.begin(),
                                            it->second.end());
    }
    IPC::Message* m = new DevToolsAgentMsg_Attach(properties);
    m->set_routing_id(inspected_rvh->routing_id());
    inspected_rvh->Send(m);
  }
}

void DevToolsManager::SendDetachToAgent(RenderViewHost* inspected_rvh) {
  if (inspected_rvh) {
    IPC::Message* m = new DevToolsAgentMsg_Detach();
    m->set_routing_id(inspected_rvh->routing_id());
    inspected_rvh->Send(m);
  }
}

void DevToolsManager::ForceReopenWindow() {
  if (inspected_rvh_for_reopen_) {
    RenderViewHost* inspected_rvn = inspected_rvh_for_reopen_;
    UnregisterDevToolsClientHostFor(inspected_rvn);
    OpenDevToolsWindow(inspected_rvn);
  }
}

DevToolsClientHost* DevToolsManager::FindOwnerDevToolsClientHost(
    RenderViewHost* client_rvh) {
  for (InspectedRvhToClientHostMap::iterator it =
           inspected_rvh_to_client_host_.begin();
       it != inspected_rvh_to_client_host_.end();
       ++it) {
    DevToolsWindow* win = it->second->AsDevToolsWindow();
    if (!win)
      continue;
    if (client_rvh == win->GetRenderViewHost())
      return it->second;
  }
  return NULL;
}

void DevToolsManager::ReopenWindow(RenderViewHost* client_rvh, bool docked) {
  DevToolsClientHost* client_host = FindOwnerDevToolsClientHost(client_rvh);
  if (!client_host)
    return;
  RenderViewHost* inspected_rvh = GetInspectedRenderViewHost(client_host);
  DCHECK(inspected_rvh);
  inspected_rvh->process()->profile()->GetPrefs()->SetBoolean(
      prefs::kDevToolsOpenDocked, docked);

  DevToolsWindow* window = client_host->AsDevToolsWindow();
  DCHECK(window);
  window->SetDocked(docked);
}

void DevToolsManager::ToggleDevToolsWindow(
    RenderViewHost* inspected_rvh,
    bool force_open,
    DevToolsToggleAction action) {
  bool do_open = force_open;
  DevToolsClientHost* host = GetDevToolsClientHostFor(inspected_rvh);

  if (host != NULL && host->AsDevToolsWindow() == NULL) {
    // Break remote debugging / extension debugging session.
    UnregisterDevToolsClientHostFor(inspected_rvh);
    host = NULL;
  }

  if (!host) {
    bool docked = inspected_rvh->process()->profile()->GetPrefs()->
        GetBoolean(prefs::kDevToolsOpenDocked);
    host = new DevToolsWindow(
        inspected_rvh->site_instance()->browsing_instance()->profile(),
        inspected_rvh,
        docked);
    RegisterDevToolsClientHostFor(inspected_rvh, host);
    do_open = true;
  }

  DevToolsWindow* window = host->AsDevToolsWindow();
  // If window is docked and visible, we hide it on toggle. If window is
  // undocked, we show (activate) it.
  if (!window->is_docked() || do_open) {
    AutoReset<bool> auto_reset_in_initial_show(&in_initial_show_, true);
    window->Show(action);
  } else {
    UnregisterDevToolsClientHostFor(inspected_rvh);
  }
}

void DevToolsManager::BindClientHost(
    RenderViewHost* inspected_rvh,
    DevToolsClientHost* client_host,
    const DevToolsRuntimeProperties& runtime_properties) {
  DCHECK(inspected_rvh_to_client_host_.find(inspected_rvh) ==
      inspected_rvh_to_client_host_.end());
  DCHECK(client_host_to_inspected_rvh_.find(client_host) ==
      client_host_to_inspected_rvh_.end());

  if (client_host_to_inspected_rvh_.empty()) {
    BrowserThread::PostTask(
        BrowserThread::IO,
        FROM_HERE,
        NewRunnableFunction(&DevToolsNetLogObserver::Attach,
                            g_browser_process->io_thread()));
  }
  inspected_rvh_to_client_host_[inspected_rvh] = client_host;
  client_host_to_inspected_rvh_[client_host] = inspected_rvh;
  runtime_properties_map_[inspected_rvh] = runtime_properties;
}

void DevToolsManager::UnbindClientHost(RenderViewHost* inspected_rvh,
                                       DevToolsClientHost* client_host) {
  DCHECK(inspected_rvh_to_client_host_.find(inspected_rvh)->second ==
      client_host);
  DCHECK(client_host_to_inspected_rvh_.find(client_host)->second ==
      inspected_rvh);

  inspected_rvh_to_client_host_.erase(inspected_rvh);
  client_host_to_inspected_rvh_.erase(client_host);
  runtime_properties_map_.erase(inspected_rvh);

  if (client_host_to_inspected_rvh_.empty()) {
    BrowserThread::PostTask(
        BrowserThread::IO,
        FROM_HERE,
        NewRunnableFunction(&DevToolsNetLogObserver::Detach));
  }
  SendDetachToAgent(inspected_rvh);
  if (inspected_rvh_for_reopen_ == inspected_rvh)
    inspected_rvh_for_reopen_ = NULL;

  int process_id = inspected_rvh->process()->id();
  for (InspectedRvhToClientHostMap::iterator it =
           inspected_rvh_to_client_host_.begin();
       it != inspected_rvh_to_client_host_.end();
       ++it) {
    if (it->first->process()->id() == process_id)
      return;
  }
  // We've disconnected from the last renderer -> revoke cookie permissions.
  ChildProcessSecurityPolicy::GetInstance()->RevokeReadRawCookies(process_id);
}

void DevToolsManager::CloseAllClientHosts() {
  std::vector<RenderViewHost*> rhvs;
  for (InspectedRvhToClientHostMap::iterator it =
           inspected_rvh_to_client_host_.begin();
       it != inspected_rvh_to_client_host_.end(); ++it) {
    rhvs.push_back(it->first);
  }
  for (std::vector<RenderViewHost*>::iterator it = rhvs.begin();
       it != rhvs.end(); ++it) {
    UnregisterDevToolsClientHostFor(*it);
  }
}