// 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 "content/browser/debugger/worker_devtools_manager.h"

#include <list>
#include <map>

#include "base/bind.h"
#include "content/browser/debugger/devtools_agent_host.h"
#include "content/browser/debugger/devtools_manager_impl.h"
#include "content/browser/debugger/worker_devtools_message_filter.h"
#include "content/browser/worker_host/worker_service_impl.h"
#include "content/common/devtools_messages.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/child_process_data.h"
#include "content/public/browser/devtools_agent_host_registry.h"
#include "content/public/common/process_type.h"
#include "third_party/WebKit/Source/WebKit/chromium/public/platform/WebCString.h"
#include "third_party/WebKit/Source/WebKit/chromium/public/WebDevToolsAgent.h"
#include "third_party/WebKit/Source/WebKit/chromium/public/platform/WebString.h"

namespace content {

// Called on the UI thread.
// static
DevToolsAgentHost* DevToolsAgentHostRegistry::GetDevToolsAgentHostForWorker(
    int worker_process_id,
    int worker_route_id) {
  return WorkerDevToolsManager::GetDevToolsAgentHostForWorker(
      worker_process_id,
      worker_route_id);
}

class WorkerDevToolsManager::AgentHosts {
public:
  static void Add(WorkerId id, WorkerDevToolsAgentHost* host) {
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    if (!instance_)
      instance_ = new AgentHosts();
    instance_->map_[id] = host;
  }
  static void Remove(WorkerId id) {
    DCHECK(instance_);
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    Instances& map = instance_->map_;
    map.erase(id);
    if (map.empty()) {
      delete instance_;
      instance_ = NULL;
    }
  }

  static WorkerDevToolsAgentHost* GetAgentHost(WorkerId id) {
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    if (!instance_)
      return NULL;
    Instances& map = instance_->map_;
    Instances::iterator it = map.find(id);
    if (it == map.end())
      return NULL;
    return it->second;
  }

private:
  AgentHosts() {
  }
  ~AgentHosts() {}

  static AgentHosts* instance_;
  typedef std::map<WorkerId, WorkerDevToolsAgentHost*> Instances;
  Instances map_;
};

WorkerDevToolsManager::AgentHosts*
    WorkerDevToolsManager::AgentHosts::instance_ = NULL;


struct WorkerDevToolsManager::TerminatedInspectedWorker {
  TerminatedInspectedWorker(WorkerId id, const GURL& url, const string16& name)
      : old_worker_id(id),
        worker_url(url),
        worker_name(name) {}
  WorkerId old_worker_id;
  GURL worker_url;
  string16 worker_name;
};


class WorkerDevToolsManager::WorkerDevToolsAgentHost
    : public DevToolsAgentHost {
 public:
  explicit WorkerDevToolsAgentHost(WorkerId worker_id)
      : worker_id_(worker_id) {
    AgentHosts::Add(worker_id, this);
    BrowserThread::PostTask(
        BrowserThread::IO,
        FROM_HERE,
        base::Bind(
            &RegisterAgent,
            worker_id.first,
            worker_id.second));
  }

  void WorkerDestroyed() {
    NotifyCloseListener();
    delete this;
  }

 private:
  virtual ~WorkerDevToolsAgentHost() {
    AgentHosts::Remove(worker_id_);
  }

  static void RegisterAgent(
      int worker_process_id,
      int worker_route_id) {
    WorkerDevToolsManager::GetInstance()->RegisterDevToolsAgentHostForWorker(
        worker_process_id, worker_route_id);
  }

  static void ForwardToWorkerDevToolsAgent(
      int worker_process_id,
      int worker_route_id,
      IPC::Message* message) {
    WorkerDevToolsManager::GetInstance()->ForwardToWorkerDevToolsAgent(
        worker_process_id, worker_route_id, *message);
  }

  // DevToolsAgentHost implementation.
  virtual void SendMessageToAgent(IPC::Message* message) OVERRIDE {
    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::Bind(
            &WorkerDevToolsAgentHost::ForwardToWorkerDevToolsAgent,
            worker_id_.first,
            worker_id_.second,
            base::Owned(message)));
  }
  virtual void NotifyClientAttaching() OVERRIDE {}
  virtual void NotifyClientDetaching() OVERRIDE {}
  virtual int GetRenderProcessId() OVERRIDE { return -1; }

  WorkerId worker_id_;

  DISALLOW_COPY_AND_ASSIGN(WorkerDevToolsAgentHost);
};


class WorkerDevToolsManager::DetachedClientHosts {
 public:
  static void WorkerReloaded(WorkerId old_id, WorkerId new_id) {
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    if (instance_ && instance_->ReattachClient(old_id, new_id))
      return;
    RemovePendingWorkerData(old_id);
  }

  static void WorkerDestroyed(WorkerId id) {
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    WorkerDevToolsAgentHost* agent = AgentHosts::GetAgentHost(id);
    if (!agent) {
      RemovePendingWorkerData(id);
      return;
    }
    DevToolsManagerImpl::GetInstance()->DispatchOnInspectorFrontend(
        agent,
        WebKit::WebDevToolsAgent::workerDisconnectedFromWorkerEvent().utf8());
    int cookie = DevToolsManagerImpl::GetInstance()->DetachClientHost(agent);
    agent->WorkerDestroyed();
    if (cookie == -1) {
      RemovePendingWorkerData(id);
      return;
    }
    if (!instance_)
      new DetachedClientHosts();
    instance_->worker_id_to_cookie_[id] = cookie;
  }

 private:
  DetachedClientHosts() {
    instance_ = this;
  }
  ~DetachedClientHosts() {
    instance_ = NULL;
  }

  bool ReattachClient(WorkerId old_id, WorkerId new_id) {
    WorkerIdToCookieMap::iterator it = worker_id_to_cookie_.find(old_id);
    if (it == worker_id_to_cookie_.end())
      return false;
    DevToolsAgentHost* agent =
        WorkerDevToolsManager::GetDevToolsAgentHostForWorker(
            new_id.first,
            new_id.second);
    DevToolsManagerImpl::GetInstance()->AttachClientHost(
        it->second,
        agent);
    worker_id_to_cookie_.erase(it);
    if (worker_id_to_cookie_.empty())
      delete this;
    return true;
  }

  static void RemovePendingWorkerData(WorkerId id) {
    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::Bind(&RemoveInspectedWorkerDataOnIOThread, id));
  }

  static void RemoveInspectedWorkerDataOnIOThread(WorkerId id) {
    WorkerDevToolsManager::GetInstance()->RemoveInspectedWorkerData(id);
  }

  static DetachedClientHosts* instance_;
  typedef std::map<WorkerId, int> WorkerIdToCookieMap;
  WorkerIdToCookieMap worker_id_to_cookie_;
};

WorkerDevToolsManager::DetachedClientHosts*
    WorkerDevToolsManager::DetachedClientHosts::instance_ = NULL;

struct WorkerDevToolsManager::InspectedWorker {
  InspectedWorker(WorkerProcessHost* host, int route_id, const GURL& url,
                  const string16& name)
      : host(host),
        route_id(route_id),
        worker_url(url),
        worker_name(name) {}
  WorkerProcessHost* const host;
  int const route_id;
  GURL worker_url;
  string16 worker_name;
};

// static
WorkerDevToolsManager* WorkerDevToolsManager::GetInstance() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
  return Singleton<WorkerDevToolsManager>::get();
}

// static
DevToolsAgentHost* WorkerDevToolsManager::GetDevToolsAgentHostForWorker(
    int worker_process_id,
    int worker_route_id) {
  WorkerId id(worker_process_id, worker_route_id);
  WorkerDevToolsAgentHost* result = AgentHosts::GetAgentHost(id);
  if (!result)
    result = new WorkerDevToolsAgentHost(id);
  return result;
}

WorkerDevToolsManager::WorkerDevToolsManager() {
}

WorkerDevToolsManager::~WorkerDevToolsManager() {
}

void WorkerDevToolsManager::WorkerCreated(
    WorkerProcessHost* worker,
    const WorkerProcessHost::WorkerInstance& instance) {
  for (TerminatedInspectedWorkers::iterator it = terminated_workers_.begin();
       it != terminated_workers_.end(); ++it) {
    if (instance.Matches(it->worker_url, it->worker_name,
                         instance.partition(),
                         instance.resource_context())) {
      worker->Send(new DevToolsAgentMsg_PauseWorkerContextOnStart(
          instance.worker_route_id()));
      WorkerId new_worker_id(worker->GetData().id, instance.worker_route_id());
      paused_workers_[new_worker_id] = it->old_worker_id;
      terminated_workers_.erase(it);
      return;
    }
  }
}

void WorkerDevToolsManager::WorkerDestroyed(
    WorkerProcessHost* worker,
    int worker_route_id) {
  InspectedWorkersList::iterator it = FindInspectedWorker(
      worker->GetData().id,
      worker_route_id);
  if (it == inspected_workers_.end())
    return;

  WorkerId worker_id(worker->GetData().id, worker_route_id);
  terminated_workers_.push_back(TerminatedInspectedWorker(
      worker_id,
      it->worker_url,
      it->worker_name));
  inspected_workers_.erase(it);
  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      base::Bind(&DetachedClientHosts::WorkerDestroyed, worker_id));
}

void WorkerDevToolsManager::WorkerContextStarted(WorkerProcessHost* process,
                                                 int worker_route_id) {
  WorkerId new_worker_id(process->GetData().id, worker_route_id);
  PausedWorkers::iterator it = paused_workers_.find(new_worker_id);
  if (it == paused_workers_.end())
    return;

  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      base::Bind(
          &DetachedClientHosts::WorkerReloaded,
          it->second,
          new_worker_id));
  paused_workers_.erase(it);
}

void WorkerDevToolsManager::RemoveInspectedWorkerData(
    const WorkerId& id) {
  for (TerminatedInspectedWorkers::iterator it = terminated_workers_.begin();
       it != terminated_workers_.end(); ++it) {
    if (it->old_worker_id == id) {
      terminated_workers_.erase(it);
      return;
    }
  }

  for (PausedWorkers::iterator it = paused_workers_.begin();
       it != paused_workers_.end(); ++it) {
    if (it->second == id) {
      SendResumeToWorker(it->first);
      paused_workers_.erase(it);
      return;
    }
  }
}

WorkerDevToolsManager::InspectedWorkersList::iterator
WorkerDevToolsManager::FindInspectedWorker(
    int host_id, int route_id) {
  InspectedWorkersList::iterator it = inspected_workers_.begin();
  while (it != inspected_workers_.end()) {
    if (it->host->GetData().id == host_id && it->route_id == route_id)
      break;
    ++it;
  }
  return it;
}

static WorkerProcessHost* FindWorkerProcess(int worker_process_id) {
  for (WorkerProcessHostIterator iter; !iter.Done(); ++iter) {
    if (iter.GetData().id == worker_process_id)
      return *iter;
  }
  return NULL;
}

void WorkerDevToolsManager::RegisterDevToolsAgentHostForWorker(
    int worker_process_id,
    int worker_route_id) {
  if (WorkerProcessHost* process = FindWorkerProcess(worker_process_id)) {
    const WorkerProcessHost::Instances& instances = process->instances();
    for (WorkerProcessHost::Instances::const_iterator i = instances.begin();
         i != instances.end(); ++i) {
      if (i->worker_route_id() == worker_route_id) {
        DCHECK(FindInspectedWorker(worker_process_id, worker_route_id) ==
               inspected_workers_.end());
        inspected_workers_.push_back(
            InspectedWorker(process, worker_route_id, i->url(), i->name()));
        return;
      }
    }
  }
  NotifyWorkerDestroyedOnIOThread(worker_process_id, worker_route_id);
}

void WorkerDevToolsManager::ForwardToDevToolsClient(
    int worker_process_id,
    int worker_route_id,
    const std::string& message) {
  if (FindInspectedWorker(worker_process_id, worker_route_id) ==
          inspected_workers_.end()) {
      NOTREACHED();
      return;
  }
  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      base::Bind(
          &ForwardToDevToolsClientOnUIThread,
          worker_process_id,
          worker_route_id,
          message));
}

void WorkerDevToolsManager::SaveAgentRuntimeState(int worker_process_id,
                                                  int worker_route_id,
                                                  const std::string& state) {
  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      base::Bind(
          &SaveAgentRuntimeStateOnUIThread,
          worker_process_id,
          worker_route_id,
          state));
}

void WorkerDevToolsManager::ForwardToWorkerDevToolsAgent(
    int worker_process_id,
    int worker_route_id,
    const IPC::Message& message) {
  InspectedWorkersList::iterator it = FindInspectedWorker(
      worker_process_id,
      worker_route_id);
  if (it == inspected_workers_.end())
    return;
  IPC::Message* msg = new IPC::Message(message);
  msg->set_routing_id(worker_route_id);
  it->host->Send(msg);
}

// static
void WorkerDevToolsManager::ForwardToDevToolsClientOnUIThread(
    int worker_process_id,
    int worker_route_id,
    const std::string& message) {
  WorkerDevToolsAgentHost* agent_host = AgentHosts::GetAgentHost(WorkerId(
      worker_process_id,
      worker_route_id));
  if (!agent_host)
    return;
  DevToolsManagerImpl::GetInstance()->DispatchOnInspectorFrontend(agent_host,
                                                                  message);
}

// static
void WorkerDevToolsManager::SaveAgentRuntimeStateOnUIThread(
    int worker_process_id,
    int worker_route_id,
    const std::string& state) {
  WorkerDevToolsAgentHost* agent_host = AgentHosts::GetAgentHost(WorkerId(
      worker_process_id,
      worker_route_id));
  if (!agent_host)
    return;
  DevToolsManagerImpl::GetInstance()->SaveAgentRuntimeState(agent_host, state);
}

// static
void WorkerDevToolsManager::NotifyWorkerDestroyedOnIOThread(
    int worker_process_id,
    int worker_route_id) {
  BrowserThread::PostTask(
      BrowserThread::UI, FROM_HERE,
      base::Bind(
          &WorkerDevToolsManager::NotifyWorkerDestroyedOnUIThread,
          worker_process_id,
          worker_route_id));
}

// static
void WorkerDevToolsManager::NotifyWorkerDestroyedOnUIThread(
    int worker_process_id,
    int worker_route_id) {
  WorkerDevToolsAgentHost* host =
      AgentHosts::GetAgentHost(WorkerId(worker_process_id, worker_route_id));
  if (host)
    host->WorkerDestroyed();
}

// static
void WorkerDevToolsManager::SendResumeToWorker(const WorkerId& id) {
  if (WorkerProcessHost* process = FindWorkerProcess(id.first))
    process->Send(new DevToolsAgentMsg_ResumeWorkerContext(id.second));
}

}  // namespace content
