// Copyright (c) 2010 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/automation/extension_port_container.h"

#include "base/logging.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/values.h"
#include "chrome/browser/automation/automation_provider.h"
#include "chrome/browser/automation/extension_automation_constants.h"
#include "chrome/browser/chrome_thread.h"
#include "chrome/browser/extensions/extension_message_service.h"
#include "chrome/browser/profile.h"
#include "chrome/browser/renderer_host/render_process_host.h"
#include "chrome/browser/renderer_host/render_view_host.h"
#include "chrome/common/notification_service.h"
#include "chrome/common/render_messages.h"
#include "chrome/test/automation/automation_messages.h"

// TODO(siggi): Find a more structured way to read and write JSON messages.

namespace ext = extension_automation_constants;

ExtensionPortContainer::ExtensionPortContainer(AutomationProvider* automation,
                                               int tab_handle) :
    automation_(automation), service_(NULL), port_id_(-1),
    tab_handle_(tab_handle) {
  service_ = automation_->profile()->GetExtensionMessageService();
  DCHECK(service_);
}

ExtensionPortContainer::~ExtensionPortContainer() {
  DCHECK_EQ(MessageLoop::current()->type(), MessageLoop::TYPE_UI);

  if (port_id_ != -1)
    service_->CloseChannel(port_id_);

  NotificationService::current()->Notify(
      NotificationType::EXTENSION_PORT_DELETED_DEBUG,
      Source<IPC::Message::Sender>(this),
      NotificationService::NoDetails());
}

bool ExtensionPortContainer::PostResponseToExternalPort(
    const std::string& message) {
  return automation_->Send(
      new AutomationMsg_ForwardMessageToExternalHost(
          0, tab_handle_, message, ext::kAutomationOrigin,
          ext::kAutomationPortResponseTarget));
}

bool ExtensionPortContainer::PostMessageToExternalPort(
    const std::string& message) {
  return automation_->Send(
      new AutomationMsg_ForwardMessageToExternalHost(
          0, tab_handle_, message,
          ext::kAutomationOrigin,
          ext::kAutomationPortRequestTarget));
}

void ExtensionPortContainer::PostMessageFromExternalPort(
    const std::string &message) {
  service_->PostMessageFromRenderer(port_id_, message);
}

bool ExtensionPortContainer::Connect(const std::string &extension_id,
                                     int process_id,
                                     int routing_id,
                                     int connection_id,
                                     const std::string& channel_name,
                                     const std::string& tab_json) {
  DCHECK_EQ(MessageLoop::current()->type(), MessageLoop::TYPE_UI);

  port_id_ = service_->OpenSpecialChannelToExtension(
      extension_id, channel_name, tab_json, this);
  if (port_id_ == -1) {
    // In this case a disconnect message has been dispatched.
    return false;
  }

  SendConnectionResponse(connection_id, port_id_);
  return true;
}

void ExtensionPortContainer::SendConnectionResponse(int connection_id,
                                                    int port_id) {
  // Compose the reply message.
  scoped_ptr<DictionaryValue> msg_dict(new DictionaryValue());
  msg_dict->SetInteger(ext::kAutomationRequestIdKey, ext::CHANNEL_OPENED);
  msg_dict->SetInteger(ext::kAutomationConnectionIdKey, connection_id);
  msg_dict->SetInteger(ext::kAutomationPortIdKey, port_id);

  std::string msg_json;
  base::JSONWriter::Write(msg_dict.get(), false, &msg_json);

  PostResponseToExternalPort(msg_json);
}

bool ExtensionPortContainer::Send(IPC::Message *message) {
  DCHECK_EQ(MessageLoop::current()->type(), MessageLoop::TYPE_UI);

  IPC_BEGIN_MESSAGE_MAP(ExtensionPortContainer, *message)
    IPC_MESSAGE_HANDLER(ViewMsg_ExtensionMessageInvoke,
                        OnExtensionMessageInvoke)
    IPC_MESSAGE_UNHANDLED_ERROR()
  IPC_END_MESSAGE_MAP()

  delete message;
  return true;
}

void ExtensionPortContainer::OnExtensionMessageInvoke(
    const std::string& function_name,
    const ListValue& args,
    bool requires_incognito_access,
    const GURL& event_url) {
  if (function_name == ExtensionMessageService::kDispatchOnMessage) {
    DCHECK_EQ(args.GetSize(), 2u);

    std::string message;
    int source_port_id;
    if (args.GetString(0, &message) && args.GetInteger(1, &source_port_id))
      OnExtensionHandleMessage(message, source_port_id);
  } else if (function_name == ExtensionMessageService::kDispatchOnDisconnect) {
    DCHECK_EQ(args.GetSize(), 1u);
    int port_id;
    if (args.GetInteger(0, &port_id))
      OnExtensionPortDisconnected(port_id);
  } else if (function_name == ExtensionMessageService::kDispatchOnConnect) {
    // Do nothing.
    // TODO(siggi): implement
  } else {
    NOTREACHED() << function_name << " shouldn't be called.";
  }
}

void ExtensionPortContainer::OnExtensionHandleMessage(
    const std::string& message, int source_port_id) {
  // Compose the reply message and fire it away.
  DictionaryValue msg_dict;
  msg_dict.SetInteger(ext::kAutomationRequestIdKey, ext::POST_MESSAGE);
  msg_dict.SetInteger(ext::kAutomationPortIdKey, port_id_);
  msg_dict.SetString(ext::kAutomationMessageDataKey, message);

  std::string msg_json;
  base::JSONWriter::Write(&msg_dict, false, &msg_json);

  PostMessageToExternalPort(msg_json);
}

void ExtensionPortContainer::OnExtensionPortDisconnected(int source_port_id) {
  // Compose the disconnect message and fire it away.
  DictionaryValue msg_dict;
  msg_dict.SetInteger(ext::kAutomationRequestIdKey, ext::CHANNEL_CLOSED);
  msg_dict.SetInteger(ext::kAutomationPortIdKey, port_id_);

  std::string msg_json;
  base::JSONWriter::Write(&msg_dict, false, &msg_json);

  PostMessageToExternalPort(msg_json);
}

bool ExtensionPortContainer::InterceptMessageFromExternalHost(
    const std::string& message, const std::string& origin,
    const std::string& target, AutomationProvider* automation,
    RenderViewHost *view_host, int tab_handle) {
  if (target != ext::kAutomationPortRequestTarget)
    return false;

  if (origin != ext::kAutomationOrigin) {
    // TODO(siggi): Should we block the message on wrong origin?
    LOG(WARNING) << "Wrong origin on automation port message " << origin;
  }

  scoped_ptr<Value> message_value(base::JSONReader::Read(message, false));
  DCHECK(message_value->IsType(Value::TYPE_DICTIONARY));
  if (!message_value->IsType(Value::TYPE_DICTIONARY))
    return true;

  DictionaryValue* message_dict =
      reinterpret_cast<DictionaryValue*>(message_value.get());

  int command = -1;
  bool got_value = message_dict->GetInteger(ext::kAutomationRequestIdKey,
                                            &command);
  DCHECK(got_value);
  if (!got_value)
    return true;

  if (command == ext::OPEN_CHANNEL) {
    // Extract the "extension_id" and "connection_id" parameters.
    std::string extension_id;
    got_value = message_dict->GetString(ext::kAutomationExtensionIdKey,
                                        &extension_id);
    DCHECK(got_value);
    if (!got_value)
      return true;

    int connection_id;
    got_value = message_dict->GetInteger(ext::kAutomationConnectionIdKey,
                                         &connection_id);
    DCHECK(got_value);
    if (!got_value)
      return true;

    std::string channel_name;
    // Channel name is optional.
    message_dict->GetString(ext::kAutomationChannelNameKey, &channel_name);

    // Tab information is optional, try to retrieve it
    // and re-flatten it to a string.
    std::string tab_json("null");
    DictionaryValue* tab = NULL;
    if (message_dict->GetDictionary(ext::kAutomationTabJsonKey, &tab))
      base::JSONWriter::Write(tab, false, &tab_json);

    int routing_id = view_host->routing_id();
    // Create the extension port and connect it.
    scoped_ptr<ExtensionPortContainer> port(
        new ExtensionPortContainer(automation, tab_handle));

    int process_id = view_host->process()->id();
    if (port->Connect(extension_id, process_id, routing_id, connection_id,
                      channel_name, tab_json)) {
      // We have a successful connection.
      automation->AddPortContainer(port.release());
    }
  } else if (command == ext::POST_MESSAGE) {
    int port_id = -1;
    got_value = message_dict->GetInteger(ext::kAutomationPortIdKey, &port_id);
    DCHECK(got_value);
    if (!got_value)
      return true;

    std::string data;
    got_value = message_dict->GetString(ext::kAutomationMessageDataKey, &data);
    DCHECK(got_value);
    if (!got_value)
      return true;

    ExtensionPortContainer* port = automation->GetPortContainer(port_id);
    DCHECK(port);
    if (port)
      port->PostMessageFromExternalPort(data);
  } else if (command == ext::CHANNEL_CLOSED) {
    int port_id = -1;
    got_value = message_dict->GetInteger(ext::kAutomationPortIdKey, &port_id);
    DCHECK(got_value);
    if (!got_value)
      return true;

    ExtensionPortContainer* port = automation->GetPortContainer(port_id);
    DCHECK(port);
    if (port) {
      // This will delete the port and notify the other end of the disconnect.
      automation->RemovePortContainer(port);
    }
  } else {
    // We don't expect other messages here.
    NOTREACHED();
  }

  return true;
}