/*
 * libjingle
 * Copyright 2013, 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 "talk/app/webrtc/webrtcsessiondescriptionfactory.h"

#include "talk/app/webrtc/jsep.h"
#include "talk/app/webrtc/jsepsessiondescription.h"
#include "talk/app/webrtc/mediaconstraintsinterface.h"
#include "talk/app/webrtc/mediastreamsignaling.h"
#include "talk/app/webrtc/webrtcsession.h"

using cricket::MediaSessionOptions;

namespace webrtc {
namespace {
static const char kFailedDueToIdentityFailed[] =
    " failed because DTLS identity request failed";

// Arbitrary constant used as common name for the identity.
// Chosen to make the certificates more readable.
static const char kWebRTCIdentityName[] = "WebRTC";

static const uint64 kInitSessionVersion = 2;

static bool CompareStream(const MediaSessionOptions::Stream& stream1,
                          const MediaSessionOptions::Stream& stream2) {
  return stream1.id < stream2.id;
}

static bool SameId(const MediaSessionOptions::Stream& stream1,
                   const MediaSessionOptions::Stream& stream2) {
  return stream1.id == stream2.id;
}

// Checks if each Stream within the |streams| has unique id.
static bool ValidStreams(const MediaSessionOptions::Streams& streams) {
  MediaSessionOptions::Streams sorted_streams = streams;
  std::sort(sorted_streams.begin(), sorted_streams.end(), CompareStream);
  MediaSessionOptions::Streams::iterator it =
      std::adjacent_find(sorted_streams.begin(), sorted_streams.end(),
                         SameId);
  return it == sorted_streams.end();
}

enum {
  MSG_CREATE_SESSIONDESCRIPTION_SUCCESS,
  MSG_CREATE_SESSIONDESCRIPTION_FAILED,
  MSG_GENERATE_IDENTITY,
};

struct CreateSessionDescriptionMsg : public rtc::MessageData {
  explicit CreateSessionDescriptionMsg(
      webrtc::CreateSessionDescriptionObserver* observer)
      : observer(observer) {
  }

  rtc::scoped_refptr<webrtc::CreateSessionDescriptionObserver> observer;
  std::string error;
  rtc::scoped_ptr<webrtc::SessionDescriptionInterface> description;
};
}  // namespace

// static
void WebRtcSessionDescriptionFactory::CopyCandidatesFromSessionDescription(
    const SessionDescriptionInterface* source_desc,
    SessionDescriptionInterface* dest_desc) {
  if (!source_desc)
    return;
  for (size_t m = 0; m < source_desc->number_of_mediasections() &&
                     m < dest_desc->number_of_mediasections(); ++m) {
    const IceCandidateCollection* source_candidates =
        source_desc->candidates(m);
    const IceCandidateCollection* dest_candidates = dest_desc->candidates(m);
    for  (size_t n = 0; n < source_candidates->count(); ++n) {
      const IceCandidateInterface* new_candidate = source_candidates->at(n);
      if (!dest_candidates->HasCandidate(new_candidate))
        dest_desc->AddCandidate(source_candidates->at(n));
    }
  }
}

WebRtcSessionDescriptionFactory::WebRtcSessionDescriptionFactory(
    rtc::Thread* signaling_thread,
    cricket::ChannelManager* channel_manager,
    MediaStreamSignaling* mediastream_signaling,
    DTLSIdentityServiceInterface* dtls_identity_service,
    WebRtcSession* session,
    const std::string& session_id,
    cricket::DataChannelType dct,
    bool dtls_enabled)
    : signaling_thread_(signaling_thread),
      mediastream_signaling_(mediastream_signaling),
      session_desc_factory_(channel_manager, &transport_desc_factory_),
      // RFC 4566 suggested a Network Time Protocol (NTP) format timestamp
      // as the session id and session version. To simplify, it should be fine
      // to just use a random number as session id and start version from
      // |kInitSessionVersion|.
      session_version_(kInitSessionVersion),
      identity_service_(dtls_identity_service),
      session_(session),
      session_id_(session_id),
      data_channel_type_(dct),
      identity_request_state_(IDENTITY_NOT_NEEDED) {
  transport_desc_factory_.set_protocol(cricket::ICEPROTO_HYBRID);
  session_desc_factory_.set_add_legacy_streams(false);
  // SRTP-SDES is disabled if DTLS is on.
  SetSdesPolicy(dtls_enabled ? cricket::SEC_DISABLED : cricket::SEC_REQUIRED);

  if (!dtls_enabled) {
    return;
  }

  if (identity_service_.get()) {
    identity_request_observer_ =
      new rtc::RefCountedObject<WebRtcIdentityRequestObserver>();

    identity_request_observer_->SignalRequestFailed.connect(
        this, &WebRtcSessionDescriptionFactory::OnIdentityRequestFailed);
    identity_request_observer_->SignalIdentityReady.connect(
        this, &WebRtcSessionDescriptionFactory::OnIdentityReady);

    if (identity_service_->RequestIdentity(kWebRTCIdentityName,
                                           kWebRTCIdentityName,
                                           identity_request_observer_)) {
      LOG(LS_VERBOSE) << "DTLS-SRTP enabled; sent DTLS identity request.";
      identity_request_state_ = IDENTITY_WAITING;
    } else {
      LOG(LS_ERROR) << "Failed to send DTLS identity request.";
      identity_request_state_ = IDENTITY_FAILED;
    }
  } else {
    identity_request_state_ = IDENTITY_WAITING;
    // Do not generate the identity in the constructor since the caller has
    // not got a chance to connect to SignalIdentityReady.
    signaling_thread_->Post(this, MSG_GENERATE_IDENTITY, NULL);
  }
}

WebRtcSessionDescriptionFactory::~WebRtcSessionDescriptionFactory() {
  transport_desc_factory_.set_identity(NULL);
}

void WebRtcSessionDescriptionFactory::CreateOffer(
    CreateSessionDescriptionObserver* observer,
    const PeerConnectionInterface::RTCOfferAnswerOptions& options) {
  cricket::MediaSessionOptions session_options;

  std::string error = "CreateOffer";
  if (identity_request_state_ == IDENTITY_FAILED) {
    error += kFailedDueToIdentityFailed;
    LOG(LS_ERROR) << error;
    PostCreateSessionDescriptionFailed(observer, error);
    return;
  }

  if (!mediastream_signaling_->GetOptionsForOffer(options,
                                                  &session_options)) {
    error += " called with invalid options.";
    LOG(LS_ERROR) << error;
    PostCreateSessionDescriptionFailed(observer, error);
    return;
  }

  if (!ValidStreams(session_options.streams)) {
    error += " called with invalid media streams.";
    LOG(LS_ERROR) << error;
    PostCreateSessionDescriptionFailed(observer, error);
    return;
  }

  if (data_channel_type_ == cricket::DCT_SCTP &&
      mediastream_signaling_->HasDataChannels()) {
    session_options.data_channel_type = cricket::DCT_SCTP;
  }

  CreateSessionDescriptionRequest request(
      CreateSessionDescriptionRequest::kOffer, observer, session_options);
  if (identity_request_state_ == IDENTITY_WAITING) {
    create_session_description_requests_.push(request);
  } else {
    ASSERT(identity_request_state_ == IDENTITY_SUCCEEDED ||
           identity_request_state_ == IDENTITY_NOT_NEEDED);
    InternalCreateOffer(request);
  }
}

void WebRtcSessionDescriptionFactory::CreateAnswer(
    CreateSessionDescriptionObserver* observer,
    const MediaConstraintsInterface* constraints) {
  std::string error = "CreateAnswer";
  if (identity_request_state_ == IDENTITY_FAILED) {
    error += kFailedDueToIdentityFailed;
    LOG(LS_ERROR) << error;
    PostCreateSessionDescriptionFailed(observer, error);
    return;
  }
  if (!session_->remote_description()) {
    error += " can't be called before SetRemoteDescription.";
    LOG(LS_ERROR) << error;
    PostCreateSessionDescriptionFailed(observer, error);
    return;
  }
  if (session_->remote_description()->type() !=
      JsepSessionDescription::kOffer) {
    error += " failed because remote_description is not an offer.";
    LOG(LS_ERROR) << error;
    PostCreateSessionDescriptionFailed(observer, error);
    return;
  }

  cricket::MediaSessionOptions options;
  if (!mediastream_signaling_->GetOptionsForAnswer(constraints, &options)) {
    error += " called with invalid constraints.";
    LOG(LS_ERROR) << error;
    PostCreateSessionDescriptionFailed(observer, error);
    return;
  }
  if (!ValidStreams(options.streams)) {
    error += " called with invalid media streams.";
    LOG(LS_ERROR) << error;
    PostCreateSessionDescriptionFailed(observer, error);
    return;
  }
  // RTP data channel is handled in MediaSessionOptions::AddStream. SCTP streams
  // are not signaled in the SDP so does not go through that path and must be
  // handled here.
  if (data_channel_type_ == cricket::DCT_SCTP) {
    options.data_channel_type = cricket::DCT_SCTP;
  }

  CreateSessionDescriptionRequest request(
      CreateSessionDescriptionRequest::kAnswer, observer, options);
  if (identity_request_state_ == IDENTITY_WAITING) {
    create_session_description_requests_.push(request);
  } else {
    ASSERT(identity_request_state_ == IDENTITY_SUCCEEDED ||
           identity_request_state_ == IDENTITY_NOT_NEEDED);
    InternalCreateAnswer(request);
  }
}

void WebRtcSessionDescriptionFactory::SetSdesPolicy(
    cricket::SecurePolicy secure_policy) {
  session_desc_factory_.set_secure(secure_policy);
}

cricket::SecurePolicy WebRtcSessionDescriptionFactory::SdesPolicy() const {
  return session_desc_factory_.secure();
}

void WebRtcSessionDescriptionFactory::OnMessage(rtc::Message* msg) {
  switch (msg->message_id) {
    case MSG_CREATE_SESSIONDESCRIPTION_SUCCESS: {
      CreateSessionDescriptionMsg* param =
          static_cast<CreateSessionDescriptionMsg*>(msg->pdata);
      param->observer->OnSuccess(param->description.release());
      delete param;
      break;
    }
    case MSG_CREATE_SESSIONDESCRIPTION_FAILED: {
      CreateSessionDescriptionMsg* param =
          static_cast<CreateSessionDescriptionMsg*>(msg->pdata);
      param->observer->OnFailure(param->error);
      delete param;
      break;
    }
    case MSG_GENERATE_IDENTITY: {
      LOG(LS_INFO) << "Generating identity.";
      SetIdentity(rtc::SSLIdentity::Generate(kWebRTCIdentityName));
      break;
    }
    default:
      ASSERT(false);
      break;
  }
}

void WebRtcSessionDescriptionFactory::InternalCreateOffer(
    CreateSessionDescriptionRequest request) {
  cricket::SessionDescription* desc(
      session_desc_factory_.CreateOffer(
          request.options,
          static_cast<cricket::BaseSession*>(session_)->local_description()));
  // RFC 3264
  // When issuing an offer that modifies the session,
  // the "o=" line of the new SDP MUST be identical to that in the
  // previous SDP, except that the version in the origin field MUST
  // increment by one from the previous SDP.

  // Just increase the version number by one each time when a new offer
  // is created regardless if it's identical to the previous one or not.
  // The |session_version_| is a uint64, the wrap around should not happen.
  ASSERT(session_version_ + 1 > session_version_);
  JsepSessionDescription* offer(new JsepSessionDescription(
      JsepSessionDescription::kOffer));
  if (!offer->Initialize(desc, session_id_,
                         rtc::ToString(session_version_++))) {
    delete offer;
    PostCreateSessionDescriptionFailed(request.observer,
                                       "Failed to initialize the offer.");
    return;
  }
  if (session_->local_description() &&
      !request.options.transport_options.ice_restart) {
    // Include all local ice candidates in the SessionDescription unless
    // the an ice restart has been requested.
    CopyCandidatesFromSessionDescription(session_->local_description(), offer);
  }
  PostCreateSessionDescriptionSucceeded(request.observer, offer);
}

void WebRtcSessionDescriptionFactory::InternalCreateAnswer(
    CreateSessionDescriptionRequest request) {
  // According to http://tools.ietf.org/html/rfc5245#section-9.2.1.1
  // an answer should also contain new ice ufrag and password if an offer has
  // been received with new ufrag and password.
  request.options.transport_options.ice_restart = session_->IceRestartPending();
  // We should pass current ssl role to the transport description factory, if
  // there is already an existing ongoing session.
  rtc::SSLRole ssl_role;
  if (session_->GetSslRole(&ssl_role)) {
    request.options.transport_options.prefer_passive_role =
        (rtc::SSL_SERVER == ssl_role);
  }

  cricket::SessionDescription* desc(session_desc_factory_.CreateAnswer(
      static_cast<cricket::BaseSession*>(session_)->remote_description(),
      request.options,
      static_cast<cricket::BaseSession*>(session_)->local_description()));
  // RFC 3264
  // If the answer is different from the offer in any way (different IP
  // addresses, ports, etc.), the origin line MUST be different in the answer.
  // In that case, the version number in the "o=" line of the answer is
  // unrelated to the version number in the o line of the offer.
  // Get a new version number by increasing the |session_version_answer_|.
  // The |session_version_| is a uint64, the wrap around should not happen.
  ASSERT(session_version_ + 1 > session_version_);
  JsepSessionDescription* answer(new JsepSessionDescription(
      JsepSessionDescription::kAnswer));
  if (!answer->Initialize(desc, session_id_,
                          rtc::ToString(session_version_++))) {
    delete answer;
    PostCreateSessionDescriptionFailed(request.observer,
                                       "Failed to initialize the answer.");
    return;
  }
  if (session_->local_description() &&
      !request.options.transport_options.ice_restart) {
    // Include all local ice candidates in the SessionDescription unless
    // the remote peer has requested an ice restart.
    CopyCandidatesFromSessionDescription(session_->local_description(), answer);
  }
  session_->ResetIceRestartLatch();
  PostCreateSessionDescriptionSucceeded(request.observer, answer);
}

void WebRtcSessionDescriptionFactory::PostCreateSessionDescriptionFailed(
    CreateSessionDescriptionObserver* observer, const std::string& error) {
  CreateSessionDescriptionMsg* msg = new CreateSessionDescriptionMsg(observer);
  msg->error = error;
  signaling_thread_->Post(this, MSG_CREATE_SESSIONDESCRIPTION_FAILED, msg);
  LOG(LS_ERROR) << "Create SDP failed: " << error;
}

void WebRtcSessionDescriptionFactory::PostCreateSessionDescriptionSucceeded(
    CreateSessionDescriptionObserver* observer,
    SessionDescriptionInterface* description) {
  CreateSessionDescriptionMsg* msg = new CreateSessionDescriptionMsg(observer);
  msg->description.reset(description);
  signaling_thread_->Post(this, MSG_CREATE_SESSIONDESCRIPTION_SUCCESS, msg);
}

void WebRtcSessionDescriptionFactory::OnIdentityRequestFailed(int error) {
  ASSERT(signaling_thread_->IsCurrent());

  LOG(LS_ERROR) << "Async identity request failed: error = " << error;
  identity_request_state_ = IDENTITY_FAILED;

  std::string msg = kFailedDueToIdentityFailed;
  while (!create_session_description_requests_.empty()) {
    const CreateSessionDescriptionRequest& request =
        create_session_description_requests_.front();
    PostCreateSessionDescriptionFailed(
        request.observer,
        ((request.type == CreateSessionDescriptionRequest::kOffer) ?
            "CreateOffer" : "CreateAnswer") + msg);
    create_session_description_requests_.pop();
  }
}

void WebRtcSessionDescriptionFactory::OnIdentityReady(
    const std::string& der_cert,
    const std::string& der_private_key) {
  ASSERT(signaling_thread_->IsCurrent());
  LOG(LS_VERBOSE) << "Identity is successfully generated.";

  std::string pem_cert = rtc::SSLIdentity::DerToPem(
      rtc::kPemTypeCertificate,
      reinterpret_cast<const unsigned char*>(der_cert.data()),
      der_cert.length());
  std::string pem_key = rtc::SSLIdentity::DerToPem(
      rtc::kPemTypeRsaPrivateKey,
      reinterpret_cast<const unsigned char*>(der_private_key.data()),
      der_private_key.length());

  rtc::SSLIdentity* identity =
      rtc::SSLIdentity::FromPEMStrings(pem_key, pem_cert);
  SetIdentity(identity);
}

void WebRtcSessionDescriptionFactory::SetIdentity(
    rtc::SSLIdentity* identity) {
  identity_request_state_ = IDENTITY_SUCCEEDED;
  SignalIdentityReady(identity);

  transport_desc_factory_.set_identity(identity);
  transport_desc_factory_.set_secure(cricket::SEC_ENABLED);

  while (!create_session_description_requests_.empty()) {
    if (create_session_description_requests_.front().type ==
        CreateSessionDescriptionRequest::kOffer) {
      InternalCreateOffer(create_session_description_requests_.front());
    } else {
      InternalCreateAnswer(create_session_description_requests_.front());
    }
    create_session_description_requests_.pop();
  }
}
}  // namespace webrtc
