/*
 * libjingle
 * Copyright 2004--2008, Google Inc.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *
 *  1. Redistributions of source code must retain the above copyright notice, 
 *     this list of conditions and the following disclaimer.
 *  2. Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *  3. The name of the author may not be used to endorse or promote products 
 *     derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "pseudotcpchannel.h"
#include "talk/p2p/base/constants.h"
#include "talk/p2p/base/transportchannel.h"
#include "talk/xmllite/xmlelement.h"
#include "tunnelsessionclient.h"
#include "webrtc/base/basicdefs.h"
#include "webrtc/base/basictypes.h"
#include "webrtc/base/common.h"
#include "webrtc/base/helpers.h"
#include "webrtc/base/logging.h"
#include "webrtc/base/stringutils.h"

namespace cricket {

const char NS_TUNNEL[] = "http://www.google.com/talk/tunnel";
const buzz::StaticQName QN_TUNNEL_DESCRIPTION = { NS_TUNNEL, "description" };
const buzz::StaticQName QN_TUNNEL_TYPE = { NS_TUNNEL, "type" };
const char CN_TUNNEL[] = "tunnel";

enum {
  MSG_CLOCK = 1,
  MSG_DESTROY,
  MSG_TERMINATE,
  MSG_EVENT,
  MSG_CREATE_TUNNEL,
};

struct EventData : public rtc::MessageData {
  int event, error;
  EventData(int ev, int err = 0) : event(ev), error(err) { }
};

struct CreateTunnelData : public rtc::MessageData {
  buzz::Jid jid;
  std::string description;
  rtc::Thread* thread;
  rtc::StreamInterface* stream;
};

extern const rtc::ConstantLabel SESSION_STATES[];

const rtc::ConstantLabel SESSION_STATES[] = {
  KLABEL(Session::STATE_INIT),
  KLABEL(Session::STATE_SENTINITIATE),
  KLABEL(Session::STATE_RECEIVEDINITIATE),
  KLABEL(Session::STATE_SENTACCEPT),
  KLABEL(Session::STATE_RECEIVEDACCEPT),
  KLABEL(Session::STATE_SENTMODIFY),
  KLABEL(Session::STATE_RECEIVEDMODIFY),
  KLABEL(Session::STATE_SENTREJECT),
  KLABEL(Session::STATE_RECEIVEDREJECT),
  KLABEL(Session::STATE_SENTREDIRECT),
  KLABEL(Session::STATE_SENTTERMINATE),
  KLABEL(Session::STATE_RECEIVEDTERMINATE),
  KLABEL(Session::STATE_INPROGRESS),
  KLABEL(Session::STATE_DEINIT),
  LASTLABEL
};

///////////////////////////////////////////////////////////////////////////////
// TunnelContentDescription
///////////////////////////////////////////////////////////////////////////////

struct TunnelContentDescription : public ContentDescription {
  std::string description;

  TunnelContentDescription(const std::string& desc) : description(desc) { }
  virtual ContentDescription* Copy() const {
    return new TunnelContentDescription(*this);
  }
};

///////////////////////////////////////////////////////////////////////////////
// TunnelSessionClientBase
///////////////////////////////////////////////////////////////////////////////

TunnelSessionClientBase::TunnelSessionClientBase(const buzz::Jid& jid,
                                SessionManager* manager, const std::string &ns)
  : jid_(jid), session_manager_(manager), namespace_(ns), shutdown_(false) {
  session_manager_->AddClient(namespace_, this);
}

TunnelSessionClientBase::~TunnelSessionClientBase() {
  shutdown_ = true;
  for (std::vector<TunnelSession*>::iterator it = sessions_.begin();
       it != sessions_.end();
       ++it) {
     Session* session = (*it)->ReleaseSession(true);
     session_manager_->DestroySession(session);
  }
  session_manager_->RemoveClient(namespace_);
}

void TunnelSessionClientBase::OnSessionCreate(Session* session, bool received) {
  LOG(LS_INFO) << "TunnelSessionClientBase::OnSessionCreate: received=" 
               << received;
  ASSERT(session_manager_->signaling_thread()->IsCurrent());
  if (received)
    sessions_.push_back(
        MakeTunnelSession(session, rtc::Thread::Current(), RESPONDER));
}

void TunnelSessionClientBase::OnSessionDestroy(Session* session) {
  LOG(LS_INFO) << "TunnelSessionClientBase::OnSessionDestroy";
  ASSERT(session_manager_->signaling_thread()->IsCurrent());
  if (shutdown_)
    return;
  for (std::vector<TunnelSession*>::iterator it = sessions_.begin();
       it != sessions_.end();
       ++it) {
    if ((*it)->HasSession(session)) {
      VERIFY((*it)->ReleaseSession(false) == session);
      sessions_.erase(it);
      return;
    }
  }
}

rtc::StreamInterface* TunnelSessionClientBase::CreateTunnel(
    const buzz::Jid& to, const std::string& description) {
  // Valid from any thread
  CreateTunnelData data;
  data.jid = to;
  data.description = description;
  data.thread = rtc::Thread::Current();
  data.stream = NULL;
  session_manager_->signaling_thread()->Send(this, MSG_CREATE_TUNNEL, &data);
  return data.stream;
}

rtc::StreamInterface* TunnelSessionClientBase::AcceptTunnel(
    Session* session) {
  ASSERT(session_manager_->signaling_thread()->IsCurrent());
  TunnelSession* tunnel = NULL;
  for (std::vector<TunnelSession*>::iterator it = sessions_.begin();
       it != sessions_.end();
       ++it) {
    if ((*it)->HasSession(session)) {
      tunnel = *it;
      break;
    }
  }
  ASSERT(tunnel != NULL);

  SessionDescription* answer = CreateAnswer(session->remote_description());
  if (answer == NULL)
    return NULL;

  session->Accept(answer);
  return tunnel->GetStream();
}

void TunnelSessionClientBase::DeclineTunnel(Session* session) {
  ASSERT(session_manager_->signaling_thread()->IsCurrent());
  session->Reject(STR_TERMINATE_DECLINE);
}

void TunnelSessionClientBase::OnMessage(rtc::Message* pmsg) {
  if (pmsg->message_id == MSG_CREATE_TUNNEL) {
    ASSERT(session_manager_->signaling_thread()->IsCurrent());
    CreateTunnelData* data = static_cast<CreateTunnelData*>(pmsg->pdata);
    SessionDescription* offer = CreateOffer(data->jid, data->description);
    if (offer == NULL) {
      return;
    }

    Session* session = session_manager_->CreateSession(jid_.Str(), namespace_);
    TunnelSession* tunnel = MakeTunnelSession(session, data->thread,
                                              INITIATOR);
    sessions_.push_back(tunnel);
    session->Initiate(data->jid.Str(), offer);
    data->stream = tunnel->GetStream();
  }
}

TunnelSession* TunnelSessionClientBase::MakeTunnelSession(
    Session* session, rtc::Thread* stream_thread,
    TunnelSessionRole /*role*/) {
  return new TunnelSession(this, session, stream_thread);
}

///////////////////////////////////////////////////////////////////////////////
// TunnelSessionClient
///////////////////////////////////////////////////////////////////////////////

TunnelSessionClient::TunnelSessionClient(const buzz::Jid& jid,
                                         SessionManager* manager,
                                         const std::string &ns)
    : TunnelSessionClientBase(jid, manager, ns) {
}

TunnelSessionClient::TunnelSessionClient(const buzz::Jid& jid,
                                         SessionManager* manager)
    : TunnelSessionClientBase(jid, manager, NS_TUNNEL) {
}

TunnelSessionClient::~TunnelSessionClient() {
}


bool TunnelSessionClient::ParseContent(SignalingProtocol protocol,
                                       const buzz::XmlElement* elem,
                                       ContentDescription** content,
                                       ParseError* error) {
  if (const buzz::XmlElement* type_elem = elem->FirstNamed(QN_TUNNEL_TYPE)) {
    *content = new TunnelContentDescription(type_elem->BodyText());
    return true;
  }
  return false;
}

bool TunnelSessionClient::WriteContent(
    SignalingProtocol protocol,
    const ContentDescription* untyped_content,
    buzz::XmlElement** elem, WriteError* error) {
  const TunnelContentDescription* content =
      static_cast<const TunnelContentDescription*>(untyped_content);

  buzz::XmlElement* root = new buzz::XmlElement(QN_TUNNEL_DESCRIPTION, true);
  buzz::XmlElement* type_elem = new buzz::XmlElement(QN_TUNNEL_TYPE);
  type_elem->SetBodyText(content->description);
  root->AddElement(type_elem);
  *elem = root;
  return true;
}

SessionDescription* NewTunnelSessionDescription(
    const std::string& content_name, ContentDescription* content) {
  SessionDescription* sdesc = new SessionDescription();
  sdesc->AddContent(content_name, NS_TUNNEL, content);
  return sdesc;
}

bool FindTunnelContent(const cricket::SessionDescription* sdesc,
                       std::string* name,
                       const TunnelContentDescription** content) {
  const ContentInfo* cinfo = sdesc->FirstContentByType(NS_TUNNEL);
  if (cinfo == NULL)
    return false;

  *name = cinfo->name;
  *content = static_cast<const TunnelContentDescription*>(
      cinfo->description);
  return true;
}

void TunnelSessionClient::OnIncomingTunnel(const buzz::Jid &jid,
                                           Session *session) {
  std::string content_name;
  const TunnelContentDescription* content = NULL;
  if (!FindTunnelContent(session->remote_description(),
                         &content_name, &content)) {
    session->Reject(STR_TERMINATE_INCOMPATIBLE_PARAMETERS);
    return;
  }

  SignalIncomingTunnel(this, jid, content->description, session);
}

SessionDescription* TunnelSessionClient::CreateOffer(
    const buzz::Jid &jid, const std::string &description) {
  SessionDescription* offer = NewTunnelSessionDescription(
      CN_TUNNEL, new TunnelContentDescription(description));
  rtc::scoped_ptr<TransportDescription> tdesc(
      session_manager_->transport_desc_factory()->CreateOffer(
          TransportOptions(), NULL));
  if (tdesc.get()) {
    offer->AddTransportInfo(TransportInfo(CN_TUNNEL, *tdesc));
  } else {
    delete offer;
    offer = NULL;
  }
  return offer;
}

SessionDescription* TunnelSessionClient::CreateAnswer(
    const SessionDescription* offer) {
  std::string content_name;
  const TunnelContentDescription* offer_tunnel = NULL;
  if (!FindTunnelContent(offer, &content_name, &offer_tunnel))
    return NULL;

  SessionDescription* answer = NewTunnelSessionDescription(
      content_name, new TunnelContentDescription(offer_tunnel->description));
  const TransportInfo* tinfo = offer->GetTransportInfoByName(content_name);
  if (tinfo) {
    const TransportDescription* offer_tdesc = &tinfo->description;
    ASSERT(offer_tdesc != NULL);
    rtc::scoped_ptr<TransportDescription> tdesc(
      session_manager_->transport_desc_factory()->CreateAnswer(
          offer_tdesc, TransportOptions(),  NULL));
    if (tdesc.get()) {
      answer->AddTransportInfo(TransportInfo(content_name, *tdesc));
    } else {
      delete answer;
      answer = NULL;
    }
  }
  return answer;
}
///////////////////////////////////////////////////////////////////////////////
// TunnelSession
///////////////////////////////////////////////////////////////////////////////

//
// Signalling thread methods
//

TunnelSession::TunnelSession(TunnelSessionClientBase* client, Session* session,
                             rtc::Thread* stream_thread)
    : client_(client), session_(session), channel_(NULL) {
  ASSERT(client_ != NULL);
  ASSERT(session_ != NULL);
  session_->SignalState.connect(this, &TunnelSession::OnSessionState);
  channel_ = new PseudoTcpChannel(stream_thread, session_);
  channel_->SignalChannelClosed.connect(this, &TunnelSession::OnChannelClosed);
}

TunnelSession::~TunnelSession() {
  ASSERT(client_ != NULL);
  ASSERT(session_ == NULL);
  ASSERT(channel_ == NULL);
}

rtc::StreamInterface* TunnelSession::GetStream() {
  ASSERT(channel_ != NULL);
  return channel_->GetStream();
}

bool TunnelSession::HasSession(Session* session) {
  ASSERT(NULL != session_);
  return (session_ == session);
}

Session* TunnelSession::ReleaseSession(bool channel_exists) {
  ASSERT(NULL != session_);
  ASSERT(NULL != channel_);
  Session* session = session_;
  session_->SignalState.disconnect(this);
  session_ = NULL;
  if (channel_exists)
    channel_->SignalChannelClosed.disconnect(this);
  channel_ = NULL;
  delete this;
  return session;
}

void TunnelSession::OnSessionState(BaseSession* session,
                                   BaseSession::State state) {
  LOG(LS_INFO) << "TunnelSession::OnSessionState("
               << rtc::nonnull(
                    rtc::FindLabel(state, SESSION_STATES), "Unknown")
               << ")";
  ASSERT(session == session_);

  switch (state) {
  case Session::STATE_RECEIVEDINITIATE:
    OnInitiate();
    break;
  case Session::STATE_SENTACCEPT:
  case Session::STATE_RECEIVEDACCEPT:
    OnAccept();
    break;
  case Session::STATE_SENTTERMINATE:
  case Session::STATE_RECEIVEDTERMINATE:
    OnTerminate();
    break;
  case Session::STATE_DEINIT:
    // ReleaseSession should have been called before this.
    ASSERT(false);
    break;
  default:
    break;
  }
}

void TunnelSession::OnInitiate() {
  ASSERT(client_ != NULL);
  ASSERT(session_ != NULL);
  client_->OnIncomingTunnel(buzz::Jid(session_->remote_name()), session_);
}

void TunnelSession::OnAccept() {
  ASSERT(channel_ != NULL);
  const ContentInfo* content =
      session_->remote_description()->FirstContentByType(NS_TUNNEL);
  ASSERT(content != NULL);
  VERIFY(channel_->Connect(
      content->name, "tcp", ICE_CANDIDATE_COMPONENT_DEFAULT));
}

void TunnelSession::OnTerminate() {
  ASSERT(channel_ != NULL);
  channel_->OnSessionTerminate(session_);
}

void TunnelSession::OnChannelClosed(PseudoTcpChannel* channel) {
  ASSERT(channel_ == channel);
  ASSERT(session_ != NULL);
  session_->Terminate();
}

///////////////////////////////////////////////////////////////////////////////

} // namespace cricket
