// 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/p2p/ipc_socket_factory.h"

#include "base/compiler_specific.h"
#include "base/message_loop.h"
#include "base/message_loop_proxy.h"
#include "content/renderer/p2p/socket_client.h"
#include "content/renderer/p2p/socket_dispatcher.h"
#include "jingle/glue/utils.h"
#include "third_party/libjingle/source/talk/base/asyncpacketsocket.h"

namespace content {

namespace {

// IpcPacketSocket implements talk_base::AsyncPacketSocket interface
// using P2PSocketClient that works over IPC-channel. It must be used
// on the thread it was created.
class IpcPacketSocket : public talk_base::AsyncPacketSocket,
                        public P2PSocketClient::Delegate {
 public:
  IpcPacketSocket();
  virtual ~IpcPacketSocket();

  // Always takes ownership of client even if initialization fails.
  bool Init(P2PSocketType type, P2PSocketClient* client,
            const talk_base::SocketAddress& local_address,
            const talk_base::SocketAddress& remote_address);

  // talk_base::AsyncPacketSocket interface.
  virtual talk_base::SocketAddress GetLocalAddress() const;
  virtual talk_base::SocketAddress GetRemoteAddress() const;
  virtual int Send(const void *pv, size_t cb);
  virtual int SendTo(const void *pv, size_t cb,
                     const talk_base::SocketAddress& addr);
  virtual int Close();
  virtual State GetState() const;
  virtual int GetOption(talk_base::Socket::Option opt, int* value);
  virtual int SetOption(talk_base::Socket::Option opt, int value);
  virtual int GetError() const;
  virtual void SetError(int error);

  // P2PSocketClient::Delegate implementation.
  virtual void OnOpen(const net::IPEndPoint& address) OVERRIDE;
  virtual void OnIncomingTcpConnection(const net::IPEndPoint& address,
                                       P2PSocketClient* client) OVERRIDE;
  virtual void OnError();
  virtual void OnDataReceived(const net::IPEndPoint& address,
                              const std::vector<char>& data) OVERRIDE;

 private:
  enum InternalState {
    IS_UNINITIALIZED,
    IS_OPENING,
    IS_OPEN,
    IS_CLOSED,
    IS_ERROR,
  };

  void InitAcceptedTcp(P2PSocketClient* client,
                       const talk_base::SocketAddress& local_address,
                       const talk_base::SocketAddress& remote_address);

  P2PSocketType type_;

  // Message loop on which this socket was created and being used.
  MessageLoop* message_loop_;

  // Corresponding P2P socket client.
  scoped_refptr<P2PSocketClient> client_;

  // Local address is allocated by the browser process, and the
  // renderer side doesn't know the address until it receives OnOpen()
  // event from the browser.
  talk_base::SocketAddress local_address_;

  // Remote address for client TCP connections.
  talk_base::SocketAddress remote_address_;

  // Current state of the object.
  InternalState state_;

  // Current error code. Valid when state_ == IS_ERROR.
  int error_;

  DISALLOW_COPY_AND_ASSIGN(IpcPacketSocket);
};

IpcPacketSocket::IpcPacketSocket()
    : type_(P2P_SOCKET_UDP),
      message_loop_(MessageLoop::current()),
      state_(IS_UNINITIALIZED),
      error_(0) {
}

IpcPacketSocket::~IpcPacketSocket() {
  if (state_ == IS_OPENING || state_ == IS_OPEN ||
      state_ == IS_ERROR) {
    Close();
  }
}

bool IpcPacketSocket::Init(P2PSocketType type, P2PSocketClient* client,
                           const talk_base::SocketAddress& local_address,
                           const talk_base::SocketAddress& remote_address) {
  DCHECK_EQ(MessageLoop::current(), message_loop_);
  DCHECK_EQ(state_, IS_UNINITIALIZED);

  type_ = type;
  client_ = client;
  local_address_ = local_address;
  remote_address_ = remote_address;
  state_ = IS_OPENING;

  net::IPEndPoint local_endpoint;
  if (!jingle_glue::SocketAddressToIPEndPoint(local_address, &local_endpoint)) {
    return false;
  }

  net::IPEndPoint remote_endpoint;
  if (!jingle_glue::SocketAddressToIPEndPoint(
          remote_address, &remote_endpoint)) {
    return false;
  }

  client_->Init(type, local_endpoint, remote_endpoint, this);

  return true;
}

void IpcPacketSocket::InitAcceptedTcp(
    P2PSocketClient* client,
    const talk_base::SocketAddress& local_address,
    const talk_base::SocketAddress& remote_address) {
  DCHECK_EQ(MessageLoop::current(), message_loop_);
  DCHECK_EQ(state_, IS_UNINITIALIZED);

  client_ = client;
  local_address_ = local_address;
  remote_address_ = remote_address;
  state_ = IS_OPEN;
  client_->set_delegate(this);
}

// talk_base::AsyncPacketSocket interface.
talk_base::SocketAddress IpcPacketSocket::GetLocalAddress() const {
  DCHECK_EQ(MessageLoop::current(), message_loop_);
  return local_address_;
}

talk_base::SocketAddress IpcPacketSocket::GetRemoteAddress() const {
  DCHECK_EQ(MessageLoop::current(), message_loop_);
  return remote_address_;
}

int IpcPacketSocket::Send(const void *data, size_t data_size) {
  DCHECK_EQ(MessageLoop::current(), message_loop_);
  return SendTo(data, data_size, remote_address_);
}

int IpcPacketSocket::SendTo(const void *data, size_t data_size,
                            const talk_base::SocketAddress& address) {
  DCHECK_EQ(MessageLoop::current(), message_loop_);

  switch (state_) {
    case IS_UNINITIALIZED:
      NOTREACHED();
      return EWOULDBLOCK;
    case IS_OPENING:
      return EWOULDBLOCK;
    case IS_CLOSED:
      return ENOTCONN;
    case IS_ERROR:
      return error_;
    case IS_OPEN:
      // Continue sending the packet.
      break;
  }

  const char* data_char = reinterpret_cast<const char*>(data);
  std::vector<char> data_vector(data_char, data_char + data_size);

  net::IPEndPoint address_chrome;
  if (!jingle_glue::SocketAddressToIPEndPoint(address, &address_chrome)) {
    // Just drop the packet if we failed to convert the address.
    return 0;
  }

  client_->Send(address_chrome, data_vector);

  // Fake successful send. The caller ignores result anyway.
  return data_size;
}

int IpcPacketSocket::Close() {
  DCHECK_EQ(MessageLoop::current(), message_loop_);

  client_->Close();
  state_ = IS_CLOSED;

  return 0;
}

talk_base::AsyncPacketSocket::State IpcPacketSocket::GetState() const {
  DCHECK_EQ(MessageLoop::current(), message_loop_);

  switch (state_) {
    case IS_UNINITIALIZED:
      NOTREACHED();
      return STATE_CLOSED;

    case IS_OPENING:
      return STATE_BINDING;

    case IS_OPEN:
      if (type_ == P2P_SOCKET_TCP_CLIENT) {
        return STATE_CONNECTED;
      } else {
        return STATE_BOUND;
      }

    case IS_CLOSED:
    case IS_ERROR:
      return STATE_CLOSED;
  }

  NOTREACHED();
  return STATE_CLOSED;
}

int IpcPacketSocket::GetOption(talk_base::Socket::Option opt, int* value) {
  // We don't support socket options for IPC sockets.
  return -1;
}

int IpcPacketSocket::SetOption(talk_base::Socket::Option opt, int value) {
  // We don't support socket options for IPC sockets.
  //
  // TODO(sergeyu): Make sure we set proper socket options on the
  // browser side.
  return -1;
}

int IpcPacketSocket::GetError() const {
  DCHECK_EQ(MessageLoop::current(), message_loop_);
  return error_;
}

void IpcPacketSocket::SetError(int error) {
  DCHECK_EQ(MessageLoop::current(), message_loop_);
  error_ = error;
}

void IpcPacketSocket::OnOpen(const net::IPEndPoint& address) {
  DCHECK_EQ(MessageLoop::current(), message_loop_);

  if (!jingle_glue::IPEndPointToSocketAddress(address, &local_address_)) {
    // Always expect correct IPv4 address to be allocated.
    NOTREACHED();
    OnError();
    return;
  }

  state_ = IS_OPEN;

  SignalAddressReady(this, local_address_);
  if (type_ == P2P_SOCKET_TCP_CLIENT)
    SignalConnect(this);
}

void IpcPacketSocket::OnIncomingTcpConnection(
    const net::IPEndPoint& address,
    P2PSocketClient* client) {
  DCHECK_EQ(MessageLoop::current(), message_loop_);

  scoped_ptr<IpcPacketSocket> socket(new IpcPacketSocket());

  talk_base::SocketAddress remote_address;
  if (!jingle_glue::IPEndPointToSocketAddress(address, &remote_address)) {
    // Always expect correct IPv4 address to be allocated.
    NOTREACHED();
  }
  socket->InitAcceptedTcp(client, local_address_, remote_address);
  SignalNewConnection(this, socket.release());
}

void IpcPacketSocket::OnError() {
  DCHECK_EQ(MessageLoop::current(), message_loop_);
  state_ = IS_ERROR;
  error_ = ECONNABORTED;
}

void IpcPacketSocket::OnDataReceived(const net::IPEndPoint& address,
                                     const std::vector<char>& data) {
  DCHECK_EQ(MessageLoop::current(), message_loop_);

  talk_base::SocketAddress address_lj;
  if (!jingle_glue::IPEndPointToSocketAddress(address, &address_lj)) {
    // We should always be able to convert address here because we
    // don't expect IPv6 address on IPv4 connections.
    NOTREACHED();
    return;
  }

  SignalReadPacket(this, &data[0], data.size(), address_lj);
}

}  // namespace

IpcPacketSocketFactory::IpcPacketSocketFactory(
    P2PSocketDispatcher* socket_dispatcher)
    : socket_dispatcher_(socket_dispatcher) {
}

IpcPacketSocketFactory::~IpcPacketSocketFactory() {
}

talk_base::AsyncPacketSocket* IpcPacketSocketFactory::CreateUdpSocket(
    const talk_base::SocketAddress& local_address, int min_port, int max_port) {
  talk_base::SocketAddress crome_address;
  P2PSocketClient* socket_client = new P2PSocketClient(socket_dispatcher_);
  scoped_ptr<IpcPacketSocket> socket(new IpcPacketSocket());
  // TODO(sergeyu): Respect local_address and port limits here (need
  // to pass them over IPC channel to the browser).
  if (!socket->Init(P2P_SOCKET_UDP, socket_client,
                    local_address, talk_base::SocketAddress())) {
    return NULL;
  }
  return socket.release();
}

talk_base::AsyncPacketSocket* IpcPacketSocketFactory::CreateServerTcpSocket(
    const talk_base::SocketAddress& local_address, int min_port, int max_port,
    bool ssl) {
  // TODO(sergeyu): Implement SSL support.
  if (ssl)
    return NULL;

  talk_base::SocketAddress crome_address;
  P2PSocketClient* socket_client = new P2PSocketClient(socket_dispatcher_);
  scoped_ptr<IpcPacketSocket> socket(new IpcPacketSocket());
  if (!socket->Init(P2P_SOCKET_TCP_SERVER, socket_client, local_address,
                    talk_base::SocketAddress())) {
    return NULL;
  }
  return socket.release();
}

talk_base::AsyncPacketSocket* IpcPacketSocketFactory::CreateClientTcpSocket(
    const talk_base::SocketAddress& local_address,
    const talk_base::SocketAddress& remote_address,
    const talk_base::ProxyInfo& proxy_info,
    const std::string& user_agent, bool ssl) {
  // TODO(sergeyu): Implement SSL support.
  if (ssl)
    return NULL;

  talk_base::SocketAddress crome_address;
  P2PSocketClient* socket_client = new P2PSocketClient(socket_dispatcher_);
  scoped_ptr<IpcPacketSocket> socket(new IpcPacketSocket());
  if (!socket->Init(P2P_SOCKET_TCP_CLIENT, socket_client, local_address,
                    remote_address))
    return NULL;
  return socket.release();
}

}  // namespace content
