// 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/devtools/worker_devtools_manager.h"

#include <list>
#include <map>

#include "base/bind.h"
#include "base/lazy_instance.h"
#include "content/browser/devtools/devtools_manager_impl.h"
#include "content/browser/devtools/devtools_protocol.h"
#include "content/browser/devtools/devtools_protocol_constants.h"
#include "content/browser/devtools/ipc_devtools_agent_host.h"
#include "content/browser/devtools/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/common/process_type.h"

namespace content {

// Called on the UI thread.
// static
scoped_refptr<DevToolsAgentHost> DevToolsAgentHost::GetForWorker(
    int worker_process_id,
    int worker_route_id) {
  return WorkerDevToolsManager::GetDevToolsAgentHostForWorker(
      worker_process_id,
      worker_route_id);
}

// Called on the UI thread.
// static
bool DevToolsAgentHost::HasForWorker(
    int worker_process_id,
    int worker_route_id) {
  return WorkerDevToolsManager::HasDevToolsAgentHostForWorker(
      worker_process_id,
      worker_route_id);
}

namespace {

typedef std::map<WorkerDevToolsManager::WorkerId,
                 WorkerDevToolsManager::WorkerDevToolsAgentHost*> AgentHosts;
base::LazyInstance<AgentHosts>::Leaky g_agent_map = LAZY_INSTANCE_INITIALIZER;
base::LazyInstance<AgentHosts>::Leaky g_orphan_map = LAZY_INSTANCE_INITIALIZER;

}  // namespace

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 IPCDevToolsAgentHost {
 public:
  explicit WorkerDevToolsAgentHost(WorkerId worker_id)
      : has_worker_id_(false) {
    SetWorkerId(worker_id, false);
  }

  void SetWorkerId(WorkerId worker_id, bool reattach) {
    worker_id_ = worker_id;
    if (!has_worker_id_)
      AddRef();  //  Balanced in ResetWorkerId.
    has_worker_id_ = true;
    g_agent_map.Get()[worker_id_] = this;

    BrowserThread::PostTask(
        BrowserThread::IO,
        FROM_HERE,
        base::Bind(
            &ConnectToWorker,
            worker_id.first,
            worker_id.second));

    if (reattach)
      Reattach(state_);
  }

  void ResetWorkerId() {
    g_agent_map.Get().erase(worker_id_);
    has_worker_id_ = false;
    Release();  //  Balanced in SetWorkerId.
  }

  void SaveAgentRuntimeState(const std::string& state) {
    state_ = state;
  }

  void ConnectionFailed() {
    NotifyCloseListener();
    // Object can be deleted here.
  }

 private:
  virtual ~WorkerDevToolsAgentHost();

  static void ConnectToWorker(
      int worker_process_id,
      int worker_route_id) {
    WorkerDevToolsManager::GetInstance()->ConnectDevToolsAgentHostToWorker(
        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);
  }

  // IPCDevToolsAgentHost implementation.
  virtual void SendMessageToAgent(IPC::Message* message) OVERRIDE {
    if (!has_worker_id_) {
      delete message;
      return;
    }
    BrowserThread::PostTask(
        BrowserThread::IO, FROM_HERE,
        base::Bind(
            &WorkerDevToolsAgentHost::ForwardToWorkerDevToolsAgent,
            worker_id_.first,
            worker_id_.second,
            base::Owned(message)));
  }

  virtual void OnClientAttached() OVERRIDE {}
  virtual void OnClientDetached() OVERRIDE {}

  bool has_worker_id_;
  WorkerId worker_id_;
  std::string state_;

  DISALLOW_COPY_AND_ASSIGN(WorkerDevToolsAgentHost);
};


class WorkerDevToolsManager::DetachedClientHosts {
 public:
  static void WorkerReloaded(WorkerId old_id, WorkerId new_id) {
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    AgentHosts::iterator it = g_orphan_map.Get().find(old_id);
    if (it != g_orphan_map.Get().end()) {
      it->second->SetWorkerId(new_id, true);
      g_orphan_map.Get().erase(old_id);
      return;
    }
    RemovePendingWorkerData(old_id);
  }

  static void WorkerDestroyed(WorkerId id) {
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
    AgentHosts::iterator it = g_agent_map.Get().find(id);
    if (it == g_agent_map.Get().end()) {
      RemovePendingWorkerData(id);
      return;
    }

    WorkerDevToolsAgentHost* agent = it->second;
    DevToolsManagerImpl* devtools_manager = DevToolsManagerImpl::GetInstance();
    if (!agent->IsAttached()) {
      // Agent has no client hosts -> delete it.
      RemovePendingWorkerData(id);
      return;
    }

    // Client host is debugging this worker agent host.
    std::string notification = DevToolsProtocol::CreateNotification(
        devtools::Worker::disconnectedFromWorker::kName, NULL)->Serialize();
    devtools_manager->DispatchOnInspectorFrontend(agent, notification);
    g_orphan_map.Get()[id] = agent;
    agent->ResetWorkerId();
  }

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

 private:
  DetachedClientHosts() {}
  ~DetachedClientHosts() {}

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

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);
  AgentHosts::iterator it = g_agent_map.Get().find(id);
  if (it == g_agent_map.Get().end())
    return new WorkerDevToolsAgentHost(id);
  return it->second;
}

// static
bool WorkerDevToolsManager::HasDevToolsAgentHostForWorker(
    int worker_process_id,
    int worker_route_id) {
  WorkerId id(worker_process_id, worker_route_id);
  return g_agent_map.Get().find(id) != g_agent_map.Get().end();
}

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::ConnectDevToolsAgentHostToWorker(
    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;
      }
    }
  }
  NotifyConnectionFailedOnIOThread(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) {
  AgentHosts::iterator it = g_agent_map.Get().find(WorkerId(worker_process_id,
                                                            worker_route_id));
  if (it == g_agent_map.Get().end())
    return;
  DevToolsManagerImpl::GetInstance()->DispatchOnInspectorFrontend(it->second,
                                                                  message);
}

// static
void WorkerDevToolsManager::SaveAgentRuntimeStateOnUIThread(
    int worker_process_id,
    int worker_route_id,
    const std::string& state) {
  AgentHosts::iterator it = g_agent_map.Get().find(WorkerId(worker_process_id,
                                                            worker_route_id));
  if (it == g_agent_map.Get().end())
    return;
  it->second->SaveAgentRuntimeState(state);
}

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

// static
void WorkerDevToolsManager::NotifyConnectionFailedOnUIThread(
    int worker_process_id,
    int worker_route_id) {
  AgentHosts::iterator it = g_agent_map.Get().find(WorkerId(worker_process_id,
                                                            worker_route_id));
  if (it != g_agent_map.Get().end())
    it->second->ConnectionFailed();
}

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

WorkerDevToolsManager::WorkerDevToolsAgentHost::~WorkerDevToolsAgentHost() {
  DetachedClientHosts::RemovePendingWorkerData(worker_id_);
  g_agent_map.Get().erase(worker_id_);
  g_orphan_map.Get().erase(worker_id_);
}

}  // namespace content