/*
 * Copyright (C) 2010-2011 Stefan Sayer
 * Copyright (C) 2012-2013 FRAFOS GmbH
 *
 * This file is part of SEMS, a free SIP media server.
 *
 * SEMS is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * For a license to use the SEMS software under conditions
 * other than those described here, or to purchase support for this
 * software, please contact iptel.org by e-mail at the following addresses:
 *    info@iptel.org
 *
 * SEMS is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "CallLeg.h"
#include "AmSessionContainer.h"
#include "AmConfig.h"
#include "ampi/MonitoringAPI.h"
#include "AmSipHeaders.h"
#include "AmUtils.h"
#include "AmRtpReceiver.h"
#include "SBCCallRegistry.h"

#define TRACE DBG

// helper functions

static const char *callStatus2str(const CallLeg::CallStatus state)
{
  static const char *disconnected = "Disconnected";
  static const char *disconnecting = "Disconnecting";
  static const char *noreply = "NoReply";
  static const char *ringing = "Ringing";
  static const char *connected = "Connected";
  static const char *unknown = "???";

  switch (state) {
    case CallLeg::Disconnected: return disconnected;
    case CallLeg::Disconnecting: return disconnecting;
    case CallLeg::NoReply: return noreply;
    case CallLeg::Ringing: return ringing;
    case CallLeg::Connected: return connected;
  }

  return unknown;
}

ReliableB2BEvent::~ReliableB2BEvent()
{
  TRACE("reliable event was %sprocessed, sending %p to %s\n",
      processed ? "" : "NOT ",
      processed ? processed_reply : unprocessed_reply,
      sender.c_str());
  if (processed) {
    if (unprocessed_reply) delete unprocessed_reply;
    if (processed_reply) AmSessionContainer::instance()->postEvent(sender, processed_reply);
  }
  else {
    if (processed_reply) delete processed_reply;
    if (unprocessed_reply) AmSessionContainer::instance()->postEvent(sender, unprocessed_reply);
  }
}

////////////////////////////////////////////////////////////////////////////////
// helper functions

enum HoldMethod { SendonlyStream, InactiveStream, ZeroedConnection };

static const string sendonly("sendonly");
static const string recvonly("recvonly");
static const string sendrecv("sendrecv");
static const string inactive("inactive");

static const string zero_connection("0.0.0.0");

/** returns true if connection is avtive.
 * Returns given default_value if the connection address is empty to cope with
 * connection address set globaly and not per media stream */
static bool connectionActive(const SdpConnection &connection, bool default_value)
{
  if (connection.address.empty()) return default_value;
  if (connection.address == zero_connection) return false;
  return true;
}

enum MediaActivity { Inactive, Sendonly, Recvonly, Sendrecv };

/** Returns true if there is no direction=inactione or sendonly attribute in
 * given media stream. It doesn't check the connection address! */
static MediaActivity getMediaActivity(const vector<SdpAttribute> &attrs, MediaActivity default_value)
{
  // go through attributes and try to find sendonly/recvonly/sendrecv/inactive
  for (std::vector<SdpAttribute>::const_iterator a = attrs.begin(); 
      a != attrs.end(); ++a)
  {
    if (a->attribute == sendonly) return Sendonly;
    if (a->attribute == inactive) return Inactive;
    if (a->attribute == recvonly) return Recvonly;
    if (a->attribute == sendrecv) return Sendrecv;
  }

  return default_value; // none of the attributes given, return (session) default
}

static MediaActivity getMediaActivity(const SdpMedia &m, MediaActivity default_value)
{
  if (m.send) {
    if (m.recv) return Sendrecv;
    else return Sendonly;
  }
  else {
    if (m.recv) return Recvonly;
  }
  return Inactive;
}

static bool isHoldRequest(AmSdp &sdp, HoldMethod &method)
{
  // set defaults from session parameters and attributes
  // inactive/sendonly/sendrecv/recvonly may be given as session attributes,
  // connection can be given for session as well
  bool connection_active = connectionActive(sdp.conn, false /* empty connection like inactive? */);
  MediaActivity session_activity = getMediaActivity(sdp.attributes, Sendrecv);

  for (std::vector<SdpMedia>::iterator m = sdp.media.begin(); 
      m != sdp.media.end(); ++m) 
  {
    if (m->port == 0) continue; // this stream is disabled, handle like inactive (?)
    if (!connectionActive(m->conn, connection_active)) {
      method = ZeroedConnection;
      continue;
    }
    switch (getMediaActivity(*m, session_activity)) {
      case Sendonly:
        method = SendonlyStream;
        continue;

      case Inactive:
        method = InactiveStream;
        continue;

      case Recvonly: // ?
      case Sendrecv:
        return false; // media stream is active
    }
  }

  if (sdp.media.empty()) {
    // no streams in the SDP, needed to set method somehow
    if (!connection_active) method = ZeroedConnection;
    else {
      switch (session_activity) {
        case Sendonly:
          method = SendonlyStream;
          break;

        case Inactive:
          method = InactiveStream;
          break;

        case Recvonly:
        case Sendrecv:
          method = InactiveStream; // well, no stream is something like InactiveStream, isn't it?
          break;

      }
    }
  }

  return true; // no active stream was found
}


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

// callee
CallLeg::CallLeg(const CallLeg* caller, AmSipDialog* p_dlg, AmSipSubscription* p_subs)
  : AmB2BSession(caller->getLocalTag(),p_dlg,p_subs),
    call_status(Disconnected),
    on_hold(false),
    hold(PreserveHoldStatus)
{
  a_leg = !caller->a_leg; // we have to be the complement

  set_sip_relay_only(false); // will be changed later on (for now we have no peer so we can't relay)

  // enable OA for the purpose of hold request detection
  if (dlg) dlg->setOAEnabled(true);
  else WARN("can't enable OA!\n");

  // code below taken from createCalleeSession

  const AmSipDialog* caller_dlg = caller->dlg;

  dlg->setLocalTag(AmSession::getNewId());
  dlg->setCallid(AmSession::getNewId());

  // take important data from A leg
  dlg->setLocalParty(caller_dlg->getRemoteParty());
  dlg->setRemoteParty(caller_dlg->getLocalParty());
  dlg->setRemoteUri(caller_dlg->getLocalUri());

/*  if (AmConfig::LogSessions) {
    INFO("Starting B2B callee session %s\n",
	 getLocalTag().c_str());
  }

  MONITORING_LOG4(other_id.c_str(), 
		  "dir",  "out",
		  "from", dlg->local_party.c_str(),
		  "to",   dlg->remote_party.c_str(),
		  "ruri", dlg->remote_uri.c_str());
*/

  // copy common RTP relay settings from A leg
  //initRTPRelay(caller);
  vector<SdpPayload> lowfi_payloads;
  setRtpRelayMode(caller->getRtpRelayMode());
  setEnableDtmfTranscoding(caller->getEnableDtmfTranscoding());
  caller->getLowFiPLs(lowfi_payloads);
  setLowFiPLs(lowfi_payloads);

 
  // A->B
  SBCCallRegistry::addCall(caller_dlg->getLocalTag(),
			   SBCCallRegistryEntry(dlg->getCallid(), dlg->getLocalTag(), ""));
  // B->A
  SBCCallRegistry::addCall(dlg->getLocalTag(),
			   SBCCallRegistryEntry(caller_dlg->getCallid(), caller_dlg->getLocalTag(), caller_dlg->getRemoteTag()));

}

// caller
CallLeg::CallLeg(AmSipDialog* p_dlg, AmSipSubscription* p_subs)
  : AmB2BSession("",p_dlg,p_subs),
    call_status(Disconnected),
    on_hold(false),
    hold(PreserveHoldStatus)
{
  a_leg = true;

  // At least in the first version we start relaying after the call is fully
  // established.  This is because of forking possibility - we can't simply
  // relay if we have one A leg and multiple B legs.
  // It is possible to start relaying before call is established if we have
  // exactly one B leg (i.e. no parallel fork happened).
  set_sip_relay_only(false);

  // enable OA for the purpose of hold request detection
  if (dlg) dlg->setOAEnabled(true);
  else WARN("can't enable OA!\n");
}
    
CallLeg::~CallLeg()
{
  // do necessary cleanup (might be needed if the call leg is destroyed other
  // way then expected)
  for (vector<OtherLegInfo>::iterator i = other_legs.begin(); i != other_legs.end(); ++i) {
    i->releaseMediaSession();
  }

  while (!pending_updates.empty()) {
    SessionUpdate *u = pending_updates.front();
    pending_updates.pop_front();
    delete u;
  }

  SBCCallRegistry::removeCall(getLocalTag());
}

void CallLeg::terminateOtherLeg()
{
  if (call_status != Connected) {
    DBG("trying to terminate other leg in %s state -> terminating the others as well\n", callStatus2str(call_status));
    // FIXME: may happen when for example reply forward fails, do we want to terminate
    // all other legs in such case?
    terminateNotConnectedLegs(); // terminates all except the one identified by other_id
  }
  
  AmB2BSession::terminateOtherLeg();

  // remove this one from the list of other legs
  for (vector<OtherLegInfo>::iterator i = other_legs.begin(); i != other_legs.end(); ++i) {
    if (i->id == getOtherId()) {
      i->releaseMediaSession();
      other_legs.erase(i);
      break;
    }
  }

  // FIXME: call disconnect if connected (to put remote on hold)?
  if (getCallStatus() != Disconnected) updateCallStatus(Disconnected); // no B legs should be remaining
}

void CallLeg::terminateNotConnectedLegs()
{
  bool found = false;
  OtherLegInfo b;

  for (vector<OtherLegInfo>::iterator i = other_legs.begin(); i != other_legs.end(); ++i) {
    if (i->id != getOtherId()) {
      i->releaseMediaSession();
      AmSessionContainer::instance()->postEvent(i->id, new B2BEvent(B2BTerminateLeg));
    }
    else {
      found = true; // other_id is there
      b = *i;
    }
  }

  // quick hack to remove all terminated entries from the list
  other_legs.clear();
  if (found) other_legs.push_back(b);
}

void CallLeg::removeOtherLeg(const string &id)
{
  if (getOtherId() == id) AmB2BSession::clear_other();

  // remove the call leg from list of B legs
  for (vector<OtherLegInfo>::iterator i = other_legs.begin(); i != other_legs.end(); ++i) {
    if (i->id == id) {
      i->releaseMediaSession();
      other_legs.erase(i);
      break;
    }
  }

  /*if (terminate) AmSessionContainer::instance()->postEvent(id, new B2BEvent(B2BTerminateLeg));*/
}

// composed for caller and callee already
void CallLeg::onB2BEvent(B2BEvent* ev)
{
  switch (ev->event_id) {

    case B2BSipReply:
      onB2BReply(dynamic_cast<B2BSipReplyEvent*>(ev));
      break;

    case ConnectLeg:
      onB2BConnect(dynamic_cast<ConnectLegEvent*>(ev));
      break;

    case ReconnectLeg:
      onB2BReconnect(dynamic_cast<ReconnectLegEvent*>(ev));
      break;

    case ReplaceLeg:
      onB2BReplace(dynamic_cast<ReplaceLegEvent*>(ev));
      break;

    case ReplaceInProgress:
      onB2BReplaceInProgress(dynamic_cast<ReplaceInProgressEvent*>(ev));
      break;

    case DisconnectLeg:
      {
        DisconnectLegEvent *dle = dynamic_cast<DisconnectLegEvent*>(ev);
        if (dle) disconnect(dle->put_remote_on_hold, dle->preserve_media_session);
      }
      break;

    case ResumeHeldLeg:
      {
        ResumeHeldEvent *e = dynamic_cast<ResumeHeldEvent*>(ev);
        if (e) resumeHeld();
      }
      break;

    case ChangeRtpModeEventId:
      {
        ChangeRtpModeEvent *e = dynamic_cast<ChangeRtpModeEvent*>(ev);
        if (e) changeRtpMode(e->new_mode, e->media);
      }
      break;

      case ApplyPendingUpdatesEventId:
        if (dynamic_cast<ApplyPendingUpdatesEvent*>(ev)) applyPendingUpdate();
        break;


    case B2BSipRequest:
      if (!sip_relay_only) {
        // disable forwarding of relayed request if we are not connected [yet]
        // (only we known that, the B leg has just delayed information about being
        // connected to us and thus it can't set)
        // Need not to be done if we have only one possible B leg so instead of
        // checking call_status we can check if sip_relay_only is set or not
        B2BSipRequestEvent *req_ev = dynamic_cast<B2BSipRequestEvent*>(ev);
        if (req_ev) req_ev->forward = false;
      }
      // continue handling in AmB2bSession

    default:
      AmB2BSession::onB2BEvent(ev);
  }
}

int CallLeg::relaySipReply(AmSipReply &reply)
{
  std::map<int,AmSipRequest>::iterator t_req = recvd_req.find(reply.cseq);

  if (t_req == recvd_req.end()) {
    ERROR("Request with CSeq %u not found in recvd_req.\n", reply.cseq);
    return 0; // ignore?
  }

  int res;
  AmSipRequest req(t_req->second);

  if ((reply.code >= 300) && (reply.code <= 305) && !reply.contact.empty()) {
    // relay with Contact in 300 - 305 redirect messages
    AmSipReply n_reply(reply);
    n_reply.hdrs += SIP_HDR_COLSP(SIP_HDR_CONTACT) + reply.contact + CRLF;

    res = relaySip(req, n_reply);
  }
  else res = relaySip(req, reply); // relay response directly

  return res;
}

bool CallLeg::setOther(const string &id, bool forward)
{
  if (getOtherId() == id) return true; // already set (needed when processing 2xx after 1xx)
  for (vector<OtherLegInfo>::iterator i = other_legs.begin(); i != other_legs.end(); ++i) {
    if (i->id == id) {
      setOtherId(id);
      clearRtpReceiverRelay(); // release old media session if set
      setMediaSession(i->media_session);
      if (forward && dlg->getOAState() == AmOfferAnswer::OA_Completed) {
        // reset OA state to offer_recived if already completed to accept new
        // B leg's SDP
        dlg->setOAState(AmOfferAnswer::OA_OfferRecved);
      }
      if (i->media_session) {
        TRACE("connecting media session: %s to %s\n", 
            dlg->getLocalTag().c_str(), getOtherId().c_str());
        i->media_session->changeSession(a_leg, this);
      }
      else {
        // media session not set, set direct mode if not set already
        if (rtp_relay_mode != AmB2BSession::RTP_Direct) setRtpRelayMode(AmB2BSession::RTP_Direct);
      }
      set_sip_relay_only(true); // relay only from now on
      return true;
    }
  }
  ERROR("%s is not in the list of other leg IDs!\n", id.c_str());
  return false; // something wrong?
}

void CallLeg::b2bInitial1xx(AmSipReply& reply, bool forward)
{
  // stop processing of 100 reply here or add Trying state to handle it without
  // remembering other_id (for now, the 100 won't get here, but to be sure...)
  // Warning: 100 reply may have to tag but forward is explicitly set to false,
  // so it can't be used to check whether it is related to a forwarded request
  // or not!
  if (reply.to_tag.empty() || reply.code == 100) return;

  if (call_status == NoReply) {
    DBG("1xx reply with to-tag received in NoReply state,"
        " changing status to Ringing and remembering the"
        " other leg ID (%s)\n", getOtherId().c_str());
    if (setOther(reply.from_tag, forward)) {
      updateCallStatus(Ringing, &reply);
      if (forward && relaySipReply(reply) != 0) stopCall(StatusChangeCause::InternalError);
    }
  }
  else {
    if (getOtherId() == reply.from_tag) {
      // we can relay this reply because it is from the same B leg from which
      // we already relayed something
      if (forward && relaySipReply(reply) != 0) stopCall(StatusChangeCause::InternalError);
    }
    else {
      // in Ringing state but the reply comes from another B leg than
      // previous 1xx reply => do not relay or process other way
      DBG("1xx reply received in %s state from another B leg, ignoring\n", callStatus2str(call_status));
    }
  }
}

void CallLeg::b2bInitial2xx(AmSipReply& reply, bool forward)
{
  if (!setOther(reply.from_tag, forward)) {
    // ignore reply which comes from non-our-peer leg?
    DBG("2xx reply received from unknown B leg, ignoring\n");
    return;
  }

  DBG("setting call status to connected with leg %s\n", getOtherId().c_str());

  // terminate all other legs than the connected one (determined by other_id)
  terminateNotConnectedLegs();

  // connect media with the other leg if RTP relay is enabled
  if (!other_legs.empty())
    other_legs.begin()->releaseMediaSession(); // remove reference hold by OtherLegInfo
  other_legs.clear(); // no need to remember the connected leg here

  onCallConnected(reply);

  if (!forward) {
    // we need to generate re-INVITE based on received SDP
    saveSessionDescription(reply.body);
    sendEstablishedReInvite();
  }
  else if (relaySipReply(reply) != 0) {
    stopCall(StatusChangeCause::InternalError);
    return;
  }
  updateCallStatus(Connected, &reply);
}

void CallLeg::onInitialReply(B2BSipReplyEvent *e)
{
    if (e->reply.code < 200) b2bInitial1xx(e->reply, e->forward);
    else if (e->reply.code < 300) b2bInitial2xx(e->reply, e->forward);
    else b2bInitialErr(e->reply, e->forward);
}

void CallLeg::b2bInitialErr(AmSipReply& reply, bool forward)
{
  if (getCallStatus() == Ringing && getOtherId() != reply.from_tag) {
    removeOtherLeg(reply.from_tag); // we don't care about this leg any more
    onBLegRefused(reply); // new B leg(s) may be added
    DBG("dropping non-ok reply, it is not from current peer\n");
    return;
  }

  DBG("clean-up after non-ok reply (reply: %d, status %s, other: %s)\n", 
      reply.code, callStatus2str(getCallStatus()),
      getOtherId().c_str());
  clearRtpReceiverRelay();
  removeOtherLeg(reply.from_tag); // we don't care about this leg any more
  updateCallStatus(NoReply, &reply);
  onBLegRefused(reply); // possible serial fork here
  set_sip_relay_only(false);

  // there are other B legs for us => wait for their responses and do not
  // relay current response
  if (!other_legs.empty()) return;

  onCallFailed(CallRefused, &reply);
  if (forward) relaySipReply(reply);

  // no other B legs, terminate
  updateCallStatus(Disconnected, &reply);
  stopCall(&reply);
}

// was for caller only
void CallLeg::onB2BReply(B2BSipReplyEvent *ev)
{
  if (!ev) {
    ERROR("BUG: invalid argument given\n");
    return;
  }

  AmSipReply& reply = ev->reply;

  TRACE("%s: B2B SIP reply %d/%d %s received in %s state\n",
      getLocalTag().c_str(),
      reply.code, reply.cseq, reply.cseq_method.c_str(),
      callStatus2str(call_status));

  // FIXME: testing est_invite_cseq is wrong! (checking in what direction or
  // what role would be needed)
  bool initial_reply = (reply.cseq_method == SIP_METH_INVITE &&
      (call_status == NoReply || call_status == Ringing) &&
      ((reply.cseq == est_invite_cseq && ev->forward) || // related to initial INVITE at our side
       (!ev->forward))); // connect not related to initial INVITE at our side

  if (initial_reply) {
    // handle relayed initial replies (replies to initiating INVITE at the other
    // side, note that this need not to be initiating INVITE at our side)

    TRACE("established CSeq: %d, forward: %s\n", est_invite_cseq, ev->forward ? "yes": "no");

    onInitialReply(ev);
  }
  else {
    // handle non-initial replies

    // reply not from our peer (might be one of the discarded ones)
    if (getOtherId() != ev->sender_ltag && getOtherId() != reply.from_tag) {
      TRACE("ignoring reply from %s in %s state, other_id = '%s'\n",
	    reply.from_tag.c_str(), callStatus2str(call_status), getOtherId().c_str());
      return;
    }

    // handle replies to other requests than the initial one
    DBG("handling reply via AmB2BSession\n");
    AmB2BSession::onB2BEvent(ev);
  }
}

// TODO: original callee's version, update
void CallLeg::onB2BConnect(ConnectLegEvent* co_ev)
{
  if (!co_ev) {
    ERROR("BUG: invalid argument given\n");
    return;
  }

  if (call_status != Disconnected) {
    ERROR("BUG: ConnectLegEvent received in %s state\n", callStatus2str(call_status));
    return;
  }

  MONITORING_LOG3(getLocalTag().c_str(), 
		  "b2b_leg", getOtherId().c_str(),
		  "to", dlg->getRemoteParty().c_str(),
		  "ruri", dlg->getRemoteUri().c_str());

  // This leg is marked as 'relay only' since the beginning because it might
  // need not to know on time that it is connected and thus should relay.
  //
  // For example: B leg received 2xx reply, relayed it to A leg and is
  // immediatelly processing in-dialog request which should be relayed, but
  // A leg didn't have chance to process the relayed reply so the B leg is not
  // connected to the A leg yet when handling the in-dialog request.
  set_sip_relay_only(true); // we should relay everything to the other leg from now

  AmMimeBody body(co_ev->body);
  try {
    updateLocalBody(body);
  } catch (const string& s) {
    relayError(SIP_METH_INVITE, co_ev->r_cseq, true, 500, SIP_REPLY_SERVER_INTERNAL_ERROR);
    throw;
  }

  int res = dlg->sendRequest(SIP_METH_INVITE, &body,
      co_ev->hdrs, SIP_FLAGS_VERBATIM);
  if (res < 0) {
    DBG("sending INVITE failed, relaying back error reply\n");
    relayError(SIP_METH_INVITE, co_ev->r_cseq, true, res);

    stopCall(StatusChangeCause::InternalError);
    return;
  }

  updateCallStatus(NoReply);

  if (co_ev->relayed_invite) {
    AmSipRequest fake_req;
    fake_req.method = SIP_METH_INVITE;
    fake_req.cseq = co_ev->r_cseq;
    relayed_req[dlg->cseq - 1] = fake_req;
    est_invite_other_cseq = co_ev->r_cseq;
  }
  else est_invite_other_cseq = 0;

  if (!co_ev->body.empty()) {
    saveSessionDescription(co_ev->body);
  }

  // save CSeq of establising INVITE
  est_invite_cseq = dlg->cseq - 1;
}

void CallLeg::onB2BReconnect(ReconnectLegEvent* ev)
{
  if (!ev) {
    ERROR("BUG: invalid argument given\n");
    return;
  }
  TRACE("handling ReconnectLegEvent, other: %s, connect to %s\n", 
	getOtherId().c_str(), ev->session_tag.c_str());

  ev->markAsProcessed();

  // release old signaling and media session
  clear_other();
  clearRtpReceiverRelay();

  // check if we aren't processing INVITE now (BLF ringing call pickup)
  AmSipRequest *invite = dlg->getUASPendingInv();
  if (invite) acceptPendingInvite(invite);

  setOtherId(ev->session_tag);
  if (ev->role == ReconnectLegEvent::A) a_leg = true;
  else a_leg = false;
  // FIXME: What about calling SBC CC modules in this case? Original CC
  // interface is called from A leg only and it might happen that we were call
  // leg A before.

  set_sip_relay_only(true); // we should relay everything to the other leg from now
  updateCallStatus(NoReply);

  // use new media session if given
  setRtpRelayMode(ev->rtp_mode);
  if (ev->media) {
    setMediaSession(ev->media);
    getMediaSession()->changeSession(a_leg, this);
  }

  MONITORING_LOG3(getLocalTag().c_str(),
		  "b2b_leg", getOtherId().c_str(),
		  "to", dlg->getRemoteParty().c_str(),
		  "ruri", dlg->getRemoteUri().c_str());

  updateSession(new Reinvite(ev->hdrs, ev->body,
        /* establishing = */ true, ev->relayed_invite, ev->r_cseq));
}

void CallLeg::onB2BReplace(ReplaceLegEvent *e)
{
  if (!e) {
    ERROR("BUG: invalid argument given\n");
    return;
  }
  e->markAsProcessed();

  ReconnectLegEvent *reconnect = e->getReconnectEvent();
  if (!reconnect) {
    ERROR("BUG: invalid ReconnectLegEvent\n");
    return;
  }

  TRACE("handling ReplaceLegEvent, other: %s, connect to %s\n", 
	getOtherId().c_str(), reconnect->session_tag.c_str());

  string id(getOtherId());
  if (id.empty()) {
    // try it with the first B leg?
    if (other_legs.empty()) {
      ERROR("BUG: there is no B leg to connect our replacement to\n");
      return;
    }
    id = other_legs[0].id;
  }

  // send session ID of the other leg to the originator
  AmSessionContainer::instance()->postEvent(reconnect->session_tag, new ReplaceInProgressEvent(id));

  // send the ReconnectLegEvent to the other leg
  AmSessionContainer::instance()->postEvent(id, reconnect);

  // remove the B leg from our B leg list
  removeOtherLeg(id);

  // commit suicide if our last B leg is stolen
  if (other_legs.empty() && getOtherId().empty()) stopCall(StatusChangeCause::Other /* FIXME? */);
}

void CallLeg::onB2BReplaceInProgress(ReplaceInProgressEvent *e)
{
  for (vector<OtherLegInfo>::iterator i = other_legs.begin(); i != other_legs.end(); ++i) {
    if (i->id.empty()) {
      // replace the temporary (invalid) session with the correct one
      i->id = e->dst_session;
      return;
    }
  }
}

void CallLeg::disconnect(bool hold_remote, bool preserve_media_session)
{
  TRACE("disconnecting call leg %s from the other\n", getLocalTag().c_str());

  switch (call_status) {
    case Disconnecting:
    case Disconnected:
      DBG("trying to disconnect already disconnected (or disconnecting) call leg\n");
      return;

    case NoReply:
    case Ringing:
      WARN("trying to disconnect in not connected state, terminating not connected legs in advance (was it intended?)\n");
      terminateNotConnectedLegs();
      // do not break, continue with following state handling!

    case Connected:
      if (!preserve_media_session) {
        // we can't stay connected (at media level) with the other leg
        clearRtpReceiverRelay();
      }
      break; // this is OK
  }

  // create new media session for us if needed
  if (getRtpRelayMode() != RTP_Direct && !preserve_media_session)
    setMediaSession(new AmB2BMedia(a_leg ? this: NULL, a_leg ? NULL : this));

  clear_other();
  set_sip_relay_only(false); // we can't relay once disconnected

  if (!hold_remote || isOnHold()) updateCallStatus(Disconnected);
  else {
    updateCallStatus(Disconnecting);
    putOnHold();
  }
}

static void sdp2body(const AmSdp &sdp, AmMimeBody &body)
{
  string body_str;
  sdp.print(body_str);

  AmMimeBody *s = body.hasContentType(SIP_APPLICATION_SDP);
  if (s) s->parse(SIP_APPLICATION_SDP, (const unsigned char*)body_str.c_str(), body_str.length());
  else body.parse(SIP_APPLICATION_SDP, (const unsigned char*)body_str.c_str(), body_str.length());
}

int CallLeg::putOnHoldImpl()
{
  if (on_hold) return -1; // no request went out

  TRACE("putting remote on hold\n");
  hold = HoldRequested;

  holdRequested();

  AmSdp sdp;
  createHoldRequest(sdp);
  updateLocalSdp(sdp);

  AmMimeBody body;
  sdp2body(sdp, body);
  if (dlg->reinvite("", &body, SIP_FLAGS_VERBATIM) != 0) {
    ERROR("re-INVITE failed\n");
    offerRejected();
    return -1;
  }
  return dlg->cseq - 1;
}

int CallLeg::resumeHeldImpl()
{
  if (!on_hold) return -1;

  try {
    TRACE("resume held remote\n");
    hold = ResumeRequested;

    resumeRequested();

    AmSdp sdp;
    createResumeRequest(sdp);
    if (sdp.media.empty()) {
      ERROR("invalid un-hold SDP, can't unhold\n");
      offerRejected();
      return -1;
    }
    updateLocalSdp(sdp);

    AmMimeBody body(established_body);
    sdp2body(sdp, body);
    if (dlg->reinvite("", &body, SIP_FLAGS_VERBATIM) != 0) {
      ERROR("re-INVITE failed\n");
      offerRejected();
      return -1;
    }
    return dlg->cseq - 1;
  }
  catch (...) {
    offerRejected();
    return -1;
  }
}

void CallLeg::holdAccepted()
{
  DBG("hold accepted on %c leg\n", a_leg?'B':'A');
  if (call_status == Disconnecting) updateCallStatus(Disconnected);
  on_hold = true;
  AmB2BMedia *ms = getMediaSession();
  if (ms) {
    DBG("holdAccepted - mute %c leg\n", a_leg?'B':'A');
    ms->mute(!a_leg); // mute the stream in other (!) leg
  }
}

void CallLeg::holdRejected()
{
  if (call_status == Disconnecting) updateCallStatus(Disconnected);
}

void CallLeg::resumeAccepted()
{
  on_hold = false;
  AmB2BMedia *ms = getMediaSession();
  if (ms) ms->unmute(!a_leg); // unmute the stream in other (!) leg
  DBG("%s: resuming held, unmuting media session %p(%s)\n", getLocalTag().c_str(), ms, !a_leg ? "A" : "B");
}

// was for caller only
void CallLeg::onInvite(const AmSipRequest& req)
{
  // do not call AmB2BSession::onInvite(req); we changed the behavior
  // this method is not called for re-INVITEs because once connected we are in
  // sip_relay_only mode and the re-INVITEs are relayed instead of processing
  // (see AmB2BSession::onSipRequest)

  if (call_status == Disconnected) { // for initial INVITE only
    est_invite_cseq = req.cseq; // remember initial CSeq
    // initialize RTP relay

    // relayed INVITE - we need to add the original INVITE to
    // list of received (relayed) requests
    recvd_req.insert(std::make_pair(req.cseq, req));
  }
}

void CallLeg::onSipRequest(const AmSipRequest& req)
{
  TRACE("%s: SIP request %d %s received in %s state\n",
      getLocalTag().c_str(),
      req.cseq, req.method.c_str(), callStatus2str(call_status));

  // we need to handle cases if there is no other leg (for example call parking)
  // Note that setting sip_relay_only to false in this case doesn't solve the
  // problem because AmB2BSession always tries to relay the request into the
  // other leg.
  if ((getCallStatus() == Disconnected || getCallStatus() == Disconnecting)
        && getOtherId().empty())
  {
    TRACE("handling request %s in disconnected state", req.method.c_str());

    // this is not correct but what is?
    // handle reINVITEs within B2B call with no other leg
    if (req.method == SIP_METH_INVITE && dlg->getStatus() == AmBasicSipDialog::Connected) {
      try {
        dlg->reply(req, 500, SIP_REPLY_SERVER_INTERNAL_ERROR);
      }
      catch(...) {
        ERROR("exception when handling INVITE in disconnected state");
        dlg->reply(req, 500, SIP_REPLY_SERVER_INTERNAL_ERROR);
        // stop the call?
      }
    }
    else AmSession::onSipRequest(req);

    if (req.method == SIP_METH_BYE) {
      stopCall(&req); // is this needed?
    }
  }
  else {
    if(getCallStatus() == Disconnected &&
       req.method == SIP_METH_BYE) {
      // seems that we have already sent/received a BYE
      // -> we'd better terminate this ASAP
      //    to avoid other confusions...
      dlg->reply(req,200,"OK");
    }
    else
      AmB2BSession::onSipRequest(req);
  }
}

void CallLeg::onSipReply(const AmSipRequest& req, const AmSipReply& reply, AmSipDialog::Status old_dlg_status)
{
  TransMap::iterator t = relayed_req.find(reply.cseq);
  bool relayed_request = (t != relayed_req.end());

  TRACE("%s: SIP reply %d/%d %s (%s) received in %s state\n",
      getLocalTag().c_str(),
      reply.code, reply.cseq, reply.cseq_method.c_str(),
      (relayed_request ? "to relayed request" : "to locally generated request"),
      callStatus2str(call_status));

#if 0
  if ((oa.hold != OA::PreserveHoldStatus) && (!relayed_request)) {
    INFO("locally generated hold/resume request replied, not handling by B2B\n");
    // local hold/resume request replied, we don't want to relay this reply to the other leg!
    // => do the necessary stuff here (copy & paste from AmB2BSession::onSipReply)
    if (reply.code < 300) {
      const AmMimeBody *sdp_part = reply.body.hasContentType(SIP_APPLICATION_SDP);
      if (sdp_part) {
        AmSdp sdp;
        if (sdp.parse((const char *)sdp_part->getPayload()) == 0) updateRemoteSdp(sdp);
      }
    }
    else if (reply.code >= 300) offerRejected();
    AmSession::onSipReply(req, reply, old_dlg_status);
    return;
  }
#endif
  if (reply.code >= 300 && reply.cseq_method == SIP_METH_INVITE) offerRejected();

  // handle final replies of session updates in progress
  if (!pending_updates.empty() && reply.code >= 200 && pending_updates.front()->hasCSeq(reply.cseq)) {
    if (reply.code == 491) {
      pending_updates.front()->reset();
      double t = get491RetryTime();
      pending_updates_timer.start(getLocalTag(), t);
      TRACE("planning to retry update operation in %gs", t);
    }
    else {
      // TODO: 503, ...
      delete pending_updates.front();
      pending_updates.pop_front();
    }
  }

  AmB2BSession::onSipReply(req, reply, old_dlg_status);

  // update internal state and call related callbacks based on received reply
  // (i.e. B leg in case of initial INVITE)
  if (reply.cseq == est_invite_cseq && reply.cseq_method == SIP_METH_INVITE &&
    (call_status == NoReply || call_status == Ringing)) {
    // reply to the initial request
    if ((reply.code > 100) && (reply.code < 200)) {
      if (((call_status == NoReply)) && (!reply.to_tag.empty()))
        updateCallStatus(Ringing, &reply);
    }
    else if ((reply.code >= 200) && (reply.code < 300)) {
      onCallConnected(reply);
      updateCallStatus(Connected, &reply);
    }
    else if (reply.code >= 300) {
      updateCallStatus(Disconnected, &reply);
      terminateLeg(); // commit suicide (don't let the master to kill us)
    }
  }

  // update call registry (unfortunately has to be done always -
  // not possible to determine if learned in this reply (?))
  if (!dlg->getRemoteTag().empty() && reply.code >= 200 && req.method == SIP_METH_INVITE) {
    SBCCallRegistry::updateCall(getOtherId(), dlg->getRemoteTag());
  }

}

// was for caller only
void CallLeg::onInvite2xx(const AmSipReply& reply)
{
  // We don't want to remember reply.cseq as est_invite_cseq, do we? It was in
  // AmB2BCallerSession but we already have initial INVITE cseq remembered and
  // we don't need to change it to last reINVITE one, right? Otherwise we should
  // remember UPDATE cseq as well because SDP may change by it as well (used
  // when handling B2BSipReply in AmB2BSession to check if reINVITE should be
  // sent).
  // 
  // est_invite_cseq = reply.cseq;

  // we don't want to handle the 2xx using AmSession so the following may be
  // unwanted for us:
  // 
  AmB2BSession::onInvite2xx(reply);
}

void CallLeg::onCancel(const AmSipRequest& req)
{
  // initial INVITE handling
  if ((call_status == Ringing) || (call_status == NoReply)) {
    if (a_leg) {
      // terminate whole B2B call if the caller receives CANCEL
      onCallFailed(CallCanceled, NULL);
      updateCallStatus(Disconnected, StatusChangeCause::Canceled);
      stopCall(StatusChangeCause::Canceled);
    }
    // else { } ... ignore for B leg
  }
}

void CallLeg::terminateLeg()
{
  AmB2BSession::terminateLeg();
}

// was for caller only
void CallLeg::onRemoteDisappeared(const AmSipReply& reply) 
{
  if (call_status == Connected) {
    // only in case we are really connected
    // (called on timeout or 481 from the remote)

    DBG("remote unreachable, ending B2BUA call\n");
    // FIXME: shouldn't be cleared in AmB2BSession as well?
    clearRtpReceiverRelay(); 
    AmB2BSession::onRemoteDisappeared(reply); // terminates the other leg
    updateCallStatus(Disconnected, &reply);
  }
}

// was for caller only
void CallLeg::onBye(const AmSipRequest& req)
{
  terminateNotConnectedLegs();
  updateCallStatus(Disconnected, &req);
  clearRtpReceiverRelay(); // FIXME: shouldn't be cleared in AmB2BSession as well?
  AmB2BSession::onBye(req);
}

void CallLeg::onOtherBye(const AmSipRequest& req)
{
  updateCallStatus(Disconnected, &req);
  AmB2BSession::onOtherBye(req);
}

void CallLeg::onNoAck(unsigned int cseq)
{
  updateCallStatus(Disconnected, StatusChangeCause::NoAck);
  AmB2BSession::onNoAck(cseq);
}

void CallLeg::onNoPrack(const AmSipRequest &req, const AmSipReply &rpl)
{
  updateCallStatus(Disconnected, StatusChangeCause::NoPrack);
  AmB2BSession::onNoPrack(req, rpl);
}

void CallLeg::onRtpTimeout()
{
  updateCallStatus(Disconnected, StatusChangeCause::RtpTimeout);
  AmB2BSession::onRtpTimeout();
}

void CallLeg::onSessionTimeout()
{
  updateCallStatus(Disconnected, StatusChangeCause::SessionTimeout);
  AmB2BSession::onSessionTimeout();
}
// AmMediaSession interface from AmMediaProcessor
int CallLeg::readStreams(unsigned long long ts, unsigned char *buffer) {
  // skip RTP processing if in Relay mode
  // (but we want to process DTMF thus we may be in media processor)
  if (getRtpRelayMode()==RTP_Relay)
    return 0;
  return AmB2BSession::readStreams(ts, buffer);
}

int CallLeg::writeStreams(unsigned long long ts, unsigned char *buffer) {
  // skip RTP processing if in Relay mode
  // (but we want to process DTMF thus we may be in media processor)
  if (getRtpRelayMode()==RTP_Relay)
    return 0;
  return AmB2BSession::writeStreams(ts, buffer);
}

void CallLeg::addNewCallee(CallLeg *callee, ConnectLegEvent *e,
			   AmB2BSession::RTPRelayMode mode)
{
  OtherLegInfo b;
  b.id = callee->getLocalTag();

  callee->setRtpRelayMode(mode);
  if (mode != RTP_Direct) {
    // do not initialise the media session with A leg to avoid unnecessary A leg
    // RTP stream creation in every B leg's media session
    if (a_leg) b.media_session = new AmB2BMedia(NULL, callee);
    else b.media_session = new AmB2BMedia(callee, NULL);
    b.media_session->addReference(); // new reference for me
    callee->setMediaSession(b.media_session);
  }
  else b.media_session = NULL;
  other_legs.push_back(b);

  if (AmConfig::LogSessions) {
    TRACE("Starting B2B callee session %s\n",
	 callee->getLocalTag().c_str()/*, invite_req.cmd.c_str()*/);
  }

  AmSipDialog* callee_dlg = callee->dlg;
  MONITORING_LOG4(b.id.c_str(),
		  "dir",  "out",
		  "from", callee_dlg->getLocalParty().c_str(),
		  "to",   callee_dlg->getRemoteParty().c_str(),
		  "ruri", callee_dlg->getRemoteUri().c_str());

  callee->start();

  AmSessionContainer* sess_cont = AmSessionContainer::instance();
  sess_cont->addSession(b.id, callee);

  // generate connect event to the newly added leg
  // Warning: correct callee's role must be already set (in constructor or so)
  TRACE("relaying connect leg event to the new leg\n");
  // other stuff than relayed INVITE should be set directly when creating callee
  // (remote_uri, remote_party is not propagated and thus B2BConnectEvent is not
  // used because it would just overwrite already set things. Note that in many
  // classes derived from AmB2BCaller[Callee]Session was a lot of things set
  // explicitly)
  AmSessionContainer::instance()->postEvent(b.id, e);

  if (call_status == Disconnected) updateCallStatus(NoReply);
}

void CallLeg::setCallStatus(CallStatus new_status)
{
  call_status = new_status;
}

const char* CallLeg::getCallStatusStr() {
  switch(getCallStatus()) {
  case Disconnected : return "Disconnected";
  case NoReply : return "NoReply";
  case Ringing : return "Ringing";
  case Connected : return "Connected";
  case Disconnecting : return "Disconnecting";
  default: return "Unknown";
  };
}

void CallLeg::updateCallStatus(CallStatus new_status, const StatusChangeCause &cause)
{
  if (new_status == Connected)
    TRACE("%s leg %s changing status from %s to %s with %s\n",
        a_leg ? "A" : "B",
        getLocalTag().c_str(),
        callStatus2str(call_status),
        callStatus2str(new_status),
        getOtherId().c_str());
  else
    TRACE("%s leg %s changing status from %s to %s\n",
        a_leg ? "A" : "B",
        getLocalTag().c_str(),
        callStatus2str(call_status),
        callStatus2str(new_status));

  setCallStatus(new_status);
  onCallStatusChange(cause);
}

void CallLeg::addExistingCallee(const string &session_tag, ReconnectLegEvent *ev)
{
  // add existing session as our B leg

  OtherLegInfo b;
  b.id = session_tag;
  if (rtp_relay_mode != RTP_Direct) {
    // do not initialise the media session with A leg to avoid unnecessary A leg
    // RTP stream creation in every B leg's media session
    b.media_session = new AmB2BMedia(NULL, NULL);
    b.media_session->addReference(); // new reference for me
  }
  else b.media_session = NULL;

  // generate connect event to the newly added leg
  TRACE("relaying re-connect leg event to the B leg\n");
  ev->setMedia(b.media_session, rtp_relay_mode);
  // TODO: what about the RTP relay and other settings? send them as well?
  if (!AmSessionContainer::instance()->postEvent(session_tag, ev)) {
    // session doesn't exist - can't connect
    INFO("the B leg to connect to (%s) doesn't exist\n", session_tag.c_str());
    if (b.media_session) {
      b.media_session->releaseReference();
      b.media_session = NULL; // ptr may not be valid any more
    }
    return;
  }

  other_legs.push_back(b);
  if (call_status == Disconnected) updateCallStatus(NoReply);
}

void CallLeg::addCallee(const string &session_tag, const AmSipRequest &relayed_invite)
{
  addExistingCallee(session_tag, new ReconnectLegEvent(getLocalTag(), relayed_invite));
}

void CallLeg::addCallee(CallLeg *callee, const string &hdrs)
{
  if (!non_hold_sdp.media.empty()) {
    // use non-hold SDP if possible
    AmMimeBody body(established_body);
    sdp2body(non_hold_sdp, body);
    addNewCallee(callee, new ConnectLegEvent(hdrs, body));
  }
  else addNewCallee(callee, new ConnectLegEvent(hdrs, established_body));
}

/*void CallLeg::addCallee(CallLeg *callee, const string &hdrs, AmB2BSession::RTPRelayMode mode)
{
  addNewCallee(callee, new ConnectLegEvent(hdrs, established_body), mode);
}*/

void CallLeg::replaceExistingLeg(const string &session_tag, const AmSipRequest &relayed_invite)
{
  // add existing session as our B leg

  OtherLegInfo b;
  b.id.clear(); // this is an invalid local tag (temporarily)
  if (rtp_relay_mode != RTP_Direct) {
    // let the other leg to set its part, we will set our once connected
    b.media_session = new AmB2BMedia(NULL, NULL);
    b.media_session->addReference(); // new reference for me
  }
  else b.media_session = NULL;

  ReplaceLegEvent *ev = new ReplaceLegEvent(getLocalTag(), relayed_invite, b.media_session, rtp_relay_mode);
  // TODO: what about the RTP relay and other settings? send them as well?
  if (!AmSessionContainer::instance()->postEvent(session_tag, ev)) {
    // session doesn't exist - can't connect
    INFO("the call leg to be replaced (%s) doesn't exist\n", session_tag.c_str());
    if (b.media_session) {
      b.media_session->releaseReference();
      b.media_session = NULL;
    }
    return;
  }

  other_legs.push_back(b);
  if (call_status == Disconnected) updateCallStatus(NoReply); // we are something like connected to another leg
}

void CallLeg::replaceExistingLeg(const string &session_tag, const string &hdrs)
{
  // add existing session as our B leg

  OtherLegInfo b;
  b.id.clear(); // this is an invalid local tag (temporarily)
  if (rtp_relay_mode != RTP_Direct) {
    // let the other leg to set its part, we will set our once connected
    b.media_session = new AmB2BMedia(NULL, NULL);
    b.media_session->addReference(); // new reference for me
  }
  else b.media_session = NULL;

  ReconnectLegEvent *rev = new ReconnectLegEvent(a_leg ? ReconnectLegEvent::B : ReconnectLegEvent::A, getLocalTag(), hdrs, established_body);
  rev->setMedia(b.media_session, rtp_relay_mode);
  ReplaceLegEvent *ev = new ReplaceLegEvent(getLocalTag(), rev);
  // TODO: what about the RTP relay and other settings? send them as well?
  if (!AmSessionContainer::instance()->postEvent(session_tag, ev)) {
    // session doesn't exist - can't connect
    INFO("the call leg to be replaced (%s) doesn't exist\n", session_tag.c_str());
    if (b.media_session) {
      b.media_session->releaseReference();
      b.media_session = NULL;
    }
    return;
  }

  other_legs.push_back(b);
  if (call_status == Disconnected) updateCallStatus(NoReply); // we are something like connected to another leg
}

void CallLeg::clear_other()
{
  removeOtherLeg(getOtherId());
  AmB2BSession::clear_other();
}

void CallLeg::stopCall(const StatusChangeCause &cause) {
  if (getCallStatus() != Disconnected) updateCallStatus(Disconnected, cause);
  terminateNotConnectedLegs();
  terminateOtherLeg();
  terminateLeg();
}

void CallLeg::changeRtpMode(RTPRelayMode new_mode)
{
  if (new_mode == rtp_relay_mode) return; // requested mode is set already

  // we don't need to send reINVITE from here, expecting caller knows what is he
  // doing (it is probably processing or generating its own reINVITE)
  // Switch from RTP_Direct to RTP_Relay is safe (no audio loss), the other can
  // be lossy because already existing media object would be destroyed.
  // FIXME: use AmB2BMedia in all RTP relay modes to avoid these problems?

  clearRtpReceiverRelay();
  setRtpRelayMode(new_mode);

  switch (getCallStatus()) {
    case CallLeg::Connected:
    case CallLeg::Disconnecting:
    case CallLeg::Disconnected:
      if (new_mode == RTP_Relay || new_mode == RTP_Transcoding)
        setMediaSession(new AmB2BMedia(a_leg ? this: NULL, a_leg ? NULL : this));
      if (!getOtherId().empty())
        relayEvent(new ChangeRtpModeEvent(new_mode, getMediaSession()));
      break;

    case CallLeg::NoReply:
    case CallLeg::Ringing:
      if (other_legs.empty()) {
        // we will receive our media session from the peer later on
        // WARNING: this means that getMediaSession called before we receive one
        // will give unusable instance (NULL for now)
        if (!getOtherId().empty())
          relayEvent(new ChangeRtpModeEvent(new_mode, getMediaSession()));
      }
      else {
        // we have to release or generate new media sessions for all our B legs
        changeOtherLegsRtpMode(new_mode);
      }
      break;
  }

  switch (dlg->getOAState()) {
    case AmOfferAnswer::OA_Completed:
    case AmOfferAnswer::OA_None:
      // must be followed by OA exchange because we can't updateLocalSdp
      // (reINVITE would be needed)
      break;

    case AmOfferAnswer::OA_OfferSent:
      TRACE("changing RTP mode after offer was sent: reINVITE needed\n");
      // TODO: plan a reINVITE
      ERROR("not implemented\n");
      break;

    case AmOfferAnswer::OA_OfferRecved:
      TRACE("changing RTP mode after offer was received\n");
      break;

    case AmOfferAnswer::__max_OA: break; // grrrr
  }
}

void CallLeg::changeRtpMode(RTPRelayMode new_mode, AmB2BMedia *new_media)
{
  // we need to process regardless old RTP mode (at least new B2B media session
  // has to be used)

  bool mode_changed = (getRtpRelayMode() != new_mode);

  clearRtpReceiverRelay();
  setRtpRelayMode(new_mode);

  switch (getCallStatus()) {
    case CallLeg::Connected:
    case CallLeg::Disconnecting:
    case CallLeg::Disconnected:
      setMediaSession(new_media);
      break;

    case CallLeg::NoReply:
    case CallLeg::Ringing:
      if (other_legs.empty()) {
        // we are not the "A leg", we can use supplied media session
        setMediaSession(new_media);
      }
      else {
        // we have to release or generate new media sessions for all our B legs
        // (ignoring supplied media)
        // WARNING: we will use the same RTP relay mode for all peer legs!
        if (mode_changed) changeOtherLegsRtpMode(new_mode);
      }
      break;
  }

  AmB2BMedia *m = getMediaSession();
  if (m) m->changeSession(a_leg, this);

  switch (dlg->getOAState()) {
    case AmOfferAnswer::OA_Completed:
    case AmOfferAnswer::OA_None:
      // must be followed by OA exchange because we can't updateLocalSdp
      // (reINVITE would be needed)
      break;

    case AmOfferAnswer::OA_OfferSent:
      TRACE("changing RTP mode/media session after offer was sent: reINVITE needed\n");
      // TODO: plan a reINVITE
      ERROR("%s: not implemented\n", getLocalTag().c_str());
      break;

    case AmOfferAnswer::OA_OfferRecved:
      TRACE("changing RTP mode/media session after offer was received\n");
      break;

    case AmOfferAnswer::__max_OA: break; // grrrr
  }

}

void CallLeg::changeOtherLegsRtpMode(RTPRelayMode new_mode)
{
  // change RTP relay mode and media session for all in other_legs
  const string &other = getOtherId();
  for (vector<OtherLegInfo>::iterator i = other_legs.begin(); i != other_legs.end(); ++i) {
    i->releaseMediaSession();

    if (new_mode != RTP_Direct) {
      i->media_session = new AmB2BMedia(NULL, NULL);
      i->media_session->addReference(); // new reference for storage

      if (other == i->id && i->media_session) {
        // if connected already with one of the legs we have to use the same
        // media session for us
        setMediaSession(i->media_session);
        if (i->media_session) i->media_session->changeSession(a_leg, this);
      }
    }

    AmSessionContainer::instance()->postEvent(i->id, new ChangeRtpModeEvent(new_mode, i->media_session));
  }
}

void CallLeg::acceptPendingInvite(AmSipRequest *invite)
{
  // reply the INVITE with fake 200 reply

  AmMimeBody *sdp = invite->body.hasContentType(SIP_APPLICATION_SDP);
  AmSdp s;
  if (!sdp || s.parse((const char*)sdp->getPayload())) {
    // no offer in the INVITE (or can't be parsed), we have to append fake offer
    // into the reply
    s.version = 0;
    s.origin.user = "sems";
    s.sessionName = "sems";
    s.conn.network = NT_IN;
    s.conn.addrType = AT_V4;
    s.conn.address = "0.0.0.0";

    s.media.push_back(SdpMedia());
    SdpMedia &m = s.media.back();
    m.type = MT_AUDIO;
    m.transport = TP_RTPAVP;
    m.send = false;
    m.recv = false;
    m.payloads.push_back(SdpPayload(0));
  }

  if (!s.conn.address.empty()) s.conn.address = "0.0.0.0";
  for (vector<SdpMedia>::iterator i = s.media.begin(); i != s.media.end(); ++i) {
    //i->port = 0;
    if (!i->conn.address.empty()) i->conn.address = "0.0.0.0";
  }

  AmMimeBody body;
  string body_str;
  s.print(body_str);
  body.parse(SIP_APPLICATION_SDP, (const unsigned char*)body_str.c_str(), body_str.length());
  try {
    updateLocalBody(body);
  } catch (...) { /* throw ? */  }

  TRACE("replying pending INVITE with body: %s\n", body_str.c_str());
  dlg->reply(*invite, 200, "OK", &body);

  if (getCallStatus() != Connected) updateCallStatus(Connected);
}

int CallLeg::reinvite(const string &hdrs, const AmMimeBody &body, bool relayed, unsigned r_cseq, bool establishing)
{
  int res;
  try {
    AmMimeBody r_body(body);
    updateLocalBody(r_body);
    res = dlg->sendRequest(SIP_METH_INVITE, &r_body, hdrs, SIP_FLAGS_VERBATIM);
  } catch (const string& s) { res = -500; }

  if (res < 0) {
    if (relayed) {
      DBG("sending re-INVITE failed, relaying back error reply\n");
      relayError(SIP_METH_INVITE, r_cseq, true, res);
    }

    DBG("sending re-INVITE failed, terminating the call\n");
    stopCall(StatusChangeCause::InternalError);
    return -1;
  }

  if (relayed) {
    AmSipRequest fake_req;
    fake_req.method = SIP_METH_INVITE;
    fake_req.cseq = r_cseq;
    relayed_req[dlg->cseq - 1] = fake_req;
    est_invite_other_cseq = r_cseq;
  }
  else est_invite_other_cseq = 0;

  saveSessionDescription(body);

  if (establishing) {
    // save CSeq of establishing INVITE
    est_invite_cseq = dlg->cseq - 1;
  }
  return dlg->cseq - 1;
}

void CallLeg::adjustOffer(AmSdp &sdp)
{
  if (hold != PreserveHoldStatus) {
    DBG("local hold/unhold request");
    // locally generated hold/unhold requests that already contain correct
    // hold/resume bodies and need not to be altered via createHoldRequest
    // hold/resumeRequested is already called
  }
  else {
    // handling B2B SDP, check for hold/unhold

    HoldMethod hm;
    // if hold request, transform to requested kind of hold and remember that hold
    // was requested with this offer
    if (isHoldRequest(sdp, hm)) {
      DBG("B2b hold request");
      holdRequested();
      alterHoldRequest(sdp);
      hold = HoldRequested;
    }
    else {
      if (on_hold) {
        DBG("B2b resume request");
        resumeRequested();
        alterResumeRequest(sdp);
        hold = ResumeRequested;
      }
    }
  }
}

void CallLeg::updateLocalSdp(AmSdp &sdp)
{
  TRACE("%s: updateLocalSdp (OA: %d)\n", getLocalTag().c_str(), dlg->getOAState());
  // handle the body based on current offer-answer status
  // (possibly update the body before sending to remote)

  // FIXME: repeated SDP (183, 200) will cause false match in OA_Completed
  // (need not to be expected with re-INVITEs asking for hold)
  if (dlg->getOAState() == AmOfferAnswer::OA_None ||
      dlg->getOAState() == AmOfferAnswer::OA_Completed)
  {
    // handling offer
    adjustOffer(sdp);
  }

  if (hold == PreserveHoldStatus && !on_hold) {
    // store non-hold SDP to be able to resumeHeld
    non_hold_sdp = sdp;
  }

  AmB2BSession::updateLocalSdp(sdp);
}

void CallLeg::offerRejected()
{
  TRACE("%s: offer rejected! (hold status: %d)", getLocalTag().c_str(), hold);
  switch (hold) {
    case HoldRequested: holdRejected(); break;
    case ResumeRequested: resumeRejected(); break;
    case PreserveHoldStatus: break;
  }
  hold = PreserveHoldStatus;
}

void CallLeg::createResumeRequest(AmSdp &sdp)
{
  // use stored non-hold SDP
  // Note: this SDP doesn't need to be correct, but established_body need not to
  // be good enough for unholding (might be held already with zero conncetions)
  if (!non_hold_sdp.media.empty()) sdp = non_hold_sdp;
  else {
    // no stored non-hold SDP
    ERROR("no stored non-hold SDP, but local resume requested\n");
    // TODO: try to use established_body here and mark properly

    // if no established body exist
    throw string("not implemented");
  }
  // do not touch the sdp otherwise (use directly B2B SDP)
}

void CallLeg::debug()
{
  DBG("call leg: %s", getLocalTag().c_str());
  DBG("\tother: %s\n", getOtherId().c_str());
  DBG("\tstatus: %s\n", callStatus2str(getCallStatus()));
  DBG("\tRTP relay mode: %d\n", rtp_relay_mode);
  DBG("\ton hold: %s\n", on_hold ? "yes" : "no");
  DBG("\toffer/answer status: %d, hold: %d\n", dlg->getOAState(), hold);

  AmB2BMedia *ms = getMediaSession();
  if (ms) ms->debug();
}

int CallLeg::onSdpCompleted(const AmSdp& offer, const AmSdp& answer)
{
  TRACE("%s: oaCompleted\n", getLocalTag().c_str());
  switch (hold) {
    case HoldRequested: holdAccepted(); break;
    case ResumeRequested: resumeAccepted(); break;
    case PreserveHoldStatus: break;
  }

  hold = PreserveHoldStatus;
  return AmB2BSession::onSdpCompleted(offer, answer);
}

void CallLeg::applyPendingUpdate()
{
  TRACE("going to apply pending updates");

  if (pending_updates.empty()) return;

  if (!canUpdateSession()) {
    TRACE("can't apply pending updates now");
    return;
  }

  TRACE("applying pending updates");

  do {
    SessionUpdate *u = pending_updates.front();
    u->apply(this);
    if (u->hasCSeq()) {
      // SIP transaction started, wait for finishing it
      break;
    }
    else {
      // the update operation hasn't started a SIP transaction so it can be
      // understood as finished
      pending_updates.pop_front();
      delete u;
    }
  } while (!pending_updates.empty());
}

void CallLeg::onTransFinished()
{
  TRACE("UAC/UAS transaction finished");
  AmB2BSession::onTransFinished();

  if (pending_updates.empty() || !canUpdateSession()) return; // there is nothing we can do now

  if (pending_updates_timer.started()) {
    TRACE("UAC/UAS transaction finished, but waiting for planned updates");
    return; // it is planned to apply the updates later on
  }

  TRACE("UAC/UAS transaction finished, try to apply pending updates");
  AmSessionContainer::instance()->postEvent(getLocalTag(), new ApplyPendingUpdatesEvent());
}

void CallLeg::updateSession(SessionUpdate *u)
{
  if (!canUpdateSession() || !pending_updates.empty()) {
    TRACE("planning session update for later");
    pending_updates.push_back(u);
  }
  else {
    u->apply(this);

    if (u->hasCSeq()) pending_updates.push_back(u); // store for failover
    else delete u; // finished
  }
}

void CallLeg::putOnHold()
{
  updateSession(new PutOnHold());
}

void CallLeg::resumeHeld()
{
  updateSession(new ResumeHeld());
}