/**
 * Copyright (C) 2004-2011 Jive Software. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.java.sipmack.sip;

import gov.nist.javax.sip.header.CSeq;

import java.text.ParseException;
import java.util.ArrayList;

import javax.sip.ClientTransaction;
import javax.sip.Dialog;
import javax.sip.InvalidArgumentException;
import javax.sip.ServerTransaction;
import javax.sip.SipException;
import javax.sip.Transaction;
import javax.sip.TransactionUnavailableException;
import javax.sip.address.Address;
import javax.sip.address.SipURI;
import javax.sip.address.URI;
import javax.sip.header.AllowHeader;
import javax.sip.header.CSeqHeader;
import javax.sip.header.CallIdHeader;
import javax.sip.header.ContactHeader;
import javax.sip.header.ContentLengthHeader;
import javax.sip.header.ContentTypeHeader;
import javax.sip.header.FromHeader;
import javax.sip.header.MaxForwardsHeader;
import javax.sip.header.ToHeader;
import javax.sip.header.ViaHeader;
import javax.sip.message.Request;
import javax.sip.message.Response;

import net.java.sipmack.common.Log;
import net.java.sipmack.sip.security.SipSecurityException;

import org.jivesoftware.spark.phone.PhoneManager;

/**
 * <p/>
 * Title: SIP COMMUNICATOR-1.1
 * </p>
 * <p/>
 * Description: JAIN-SIP-1.1 Audio/Video Phone Application
 * </p>
 * <p/>
 * Copyright: Copyright (c) 2003
 * </p>
 * <p/>
 * Company: Organisation: LSIIT Laboratory (http://lsiit.u-strasbg.fr)
 * <p/>
 * </p>
 * Network Research Team (http://www-r2.u-strasbg.fr))
 * <p/>
 * </p>
 * Louis Pasteur University - Strasbourg - France
 * </p>
 *
 * @author Emil Ivov
 * @version 1.1
 */

public class CallProcessing {
    protected SipManager sipManCallback = null;

    protected CallDispatcher callDispatcher = new CallDispatcher();

    CallProcessing() {
    }

    CallProcessing(SipManager sipManCallback) {
        this.sipManCallback = sipManCallback;
    }

    void setSipManagerCallBack(SipManager sipManCallback) {
        this.sipManCallback = sipManCallback;
    }

    // ============================= Remotely Initiated Processing
    // ===================================
    // ----------------------------- Responses
    void processTrying(ClientTransaction clientTransaction, Response response) {
        // find the call
        Call call = callDispatcher.findCall(clientTransaction.getDialog());
        if (call == null) {
            sipManCallback.fireUnknownMessageReceived(response);
            return;
        }
        // change status
        if (!call.getState().equals(Call.MOVING_LOCALLY))
            call.setState(Call.DIALING);
    }

    void processRinging(ClientTransaction clientTransaction, Response response) {
        // find the call
        Call call = callDispatcher.findCall(clientTransaction.getDialog());
        String sdp = response.getRawContent() != null ? response
                .getRawContent().toString() : "";
        call.setRemoteSdpDescription(sdp);

        // change status
        call.setState(Call.RINGING);
    }

    void processRingingBack(ClientTransaction clientTransaction,
                            Response response) {
        // find the call
        Call call = callDispatcher.findCall(clientTransaction.getDialog());
        call.setRemoteSdpDescription(new String(response.getRawContent()));
        // change status
        call.setState(Call.RINGING);

    }

    /**
     * According to the RFC a UAC canceling a request cannot rely on receiving a
     * 487 (Request Terminated) response for the original request, as an RFC
     * 2543- compliant UAS will not generate such a response. So we are closing
     * the call when sending the cancel request and here we don't do anything.
     *
     * @param clientTransaction
     * @param response
     */
    void processRequestTerminated(ClientTransaction clientTransaction,
                                  Response response) {
        try {
            // add any additional code here
        }
        finally {

        }
    }

    void processByeOK(ClientTransaction clientTransaction, Response response) {
        try {
            // add any additional code here
        }
        finally {

        }
    }

    void processCancelOK(ClientTransaction clientTransaction, Response response) {
        try {
            // add any additional code here
        }
        finally {

        }
    }

    void processInviteOK(ClientTransaction clientTransaction, Response ok) {
        // find the call
        Call call = callDispatcher.findCall(clientTransaction.getDialog());
        if (call == null) {
            sipManCallback.fireUnknownMessageReceived(ok);
            return;
        }
        // Send ACK
        try {
            // Need to use dialog generated ACKs so that the remote UA core
            // sees them - Fixed by M.Ranganathan
            Request ackRequest = clientTransaction.getDialog()
					 			.createAck(((CSeqHeader)ok.getHeader(CSeqHeader.NAME)).getSeqNumber());

            clientTransaction.getDialog().sendAck(ackRequest);
            
        }
        catch (SipException ex) {
        	ex.printStackTrace();
            call.setState(Call.DISCONNECTED);
            sipManCallback
                    .fireCommunicationsError(new CommunicationsException(
                            "Failed to acknowledge call!", ex));
            return;
        } catch (InvalidArgumentException e) {
			e.printStackTrace();
			call.setState(Call.DISCONNECTED);
	            sipManCallback.fireCommunicationsError(new CommunicationsException(
	                            "Failed to create ack!", e));
	            return;
		}
        call.setRemoteSdpDescription(new String(ok.getRawContent()));
        // change status
        if (!call.getState().equals(Call.CONNECTED)) {
            call.setState(Call.CONNECTED);
        }
    }

    void processBusyHere(ClientTransaction clientTransaction, Response busyHere) {
        // find the call
        Call call = callDispatcher.findCall(clientTransaction.getDialog());
        if (call == null) {
            sipManCallback.fireUnknownMessageReceived(busyHere);
            return;
        }
        // change status
        call.setState(Call.BUSY);
        // it is the stack that should be sending the ACK so don't do it
        // here
    }

    void processCallError(ClientTransaction clientTransaction,
                          Response notAcceptable) {
        // find the call
        Call call = callDispatcher.findCall(clientTransaction.getDialog());
        if (call == null) {
            sipManCallback.fireUnknownMessageReceived(notAcceptable);
            return;
        }
        // change status
        call.setState(Call.FAILED);
        sipManCallback.fireCommunicationsError(new CommunicationsException(
                "Remote party returned error response: "
                        + notAcceptable.getStatusCode() + " - "
                        + notAcceptable.getReasonPhrase()));
        return;

        // it is the stack that should be sending the ACK so don't do it
        // here
    }

    /**
     * Attempts to re-ogenerate the corresponding request with the proper
     * credentials and terminates the call if it fails.
     *
     * @param clientTransaction the corresponding transaction
     * @param response          the challenge
     */
    void processAuthenticationChallenge(ClientTransaction clientTransaction,
                                        Response response) {
        try {
            ClientTransaction retryTran = sipManCallback.sipSecurityManager
                    .handleChallenge(response, clientTransaction);
            // There is a new dialog that will be started with this request. Get
            // that dialog and record it into the Call objet for later use (by
            // Bye-s for example).

            Call call = callDispatcher.findCall(clientTransaction.getDialog());
            call.setDialog(retryTran.getDialog());
            call.setInitialRequest(retryTran.getRequest());
            retryTran.sendRequest();
        }
        catch (SipSecurityException exc) {
            callDispatcher.findCall(clientTransaction.getDialog()).setState(
                    Call.FAILED);
            sipManCallback.fireCommunicationsError(new CommunicationsException(
                    "Authorization failed!", exc));
        }
        catch (Exception exc) {
            callDispatcher.findCall(clientTransaction.getDialog()).setState(
                    Call.FAILED);
            sipManCallback.fireCommunicationsError(new CommunicationsException(
                    "Failed to resend a request "
                            + "after a security challenge!", exc));
        }
    }

    // -------------------------- Requests ---------------------------------
    void processInvite(ServerTransaction serverTransaction, Request invite) {
        Dialog dialog = serverTransaction.getDialog();

        if (!sipManCallback.isBusy() && !(PhoneManager.isUseStaticLocator()&&PhoneManager.isUsingMediaLocator())) {
            Call call = callDispatcher.createCall(dialog, invite);
            sipManCallback.fireCallReceived(call);
            // change status
            call.setState(Call.ALERTING);
            // sdp description may be in acks - bug report Laurent Michel
            ContentLengthHeader cl = invite.getContentLength();
            if (cl != null && cl.getContentLength() > 0) {
                call.setRemoteSdpDescription(new String(invite
                        .getRawContent()));
            }
            // Are we the one they are looking for?
            URI calleeURI = ((ToHeader) invite.getHeader(ToHeader.NAME))
                    .getAddress().getURI();
            /**
             * @todo We shoud rather ask the user what to do here as some
             *       would add prefixes or change user URIs
             */
            if (calleeURI.isSipURI()) {
                boolean assertUserMatch = SIPConfig
                        .isFailCallInUserMismatch();
                // user info is case sensitive according to rfc3261
                if (assertUserMatch) {
                    String calleeUser = ((SipURI) calleeURI).getUser();
                    String localUser = sipManCallback.getLocalUser();
                    if (calleeUser != null && !calleeUser.equals(localUser)) {
                        sipManCallback
                                .fireCallRejectedLocally(
                                        "The user specified by the caller did not match the local user!",
                                        invite, call);
                        call.setState(Call.DISCONNECTED);
                        Response notFound = null;
                        try {
                            notFound = sipManCallback.messageFactory
                                    .createResponse(Response.NOT_FOUND,
                                            invite);
                            sipManCallback.attachToTag(notFound, dialog);
                        }
                        catch (ParseException ex) {
                            call.setState(Call.DISCONNECTED);
                            sipManCallback
                                    .fireCommunicationsError(new CommunicationsException(
                                            "Failed to create a NOT_FOUND response to an INVITE request!",
                                            ex));
                            return;
                        }
                        try {
                            serverTransaction.sendResponse(notFound);

                        }
                        catch (SipException ex) {
                            call.setState(Call.DISCONNECTED);
                            sipManCallback
                                    .fireCommunicationsError(new CommunicationsException(
                                            "Failed to send a NOT_FOUND response to an INVITE request!",
                                            ex));
                            return;
                        } catch (InvalidArgumentException e) {
                            call.setState(Call.DISCONNECTED);
                            sipManCallback
                                    .fireCommunicationsError(new CommunicationsException(
                                            "Failed to send a NOT_FOUND response to an INVITE request!",
                                            e));
                            return;
                        }
                        return;
                    }
                }
            }

            // Send RINGING
            Response ringing = null;
            try {
                ringing = sipManCallback.messageFactory.createResponse(
                        Response.RINGING, invite);
                sipManCallback.attachToTag(ringing, dialog);
            }
            catch (ParseException ex) {
                call.setState(Call.DISCONNECTED);
                sipManCallback
                        .fireCommunicationsError(new CommunicationsException(
                                "Failed to create a RINGING response to an INVITE request!",
                                ex));
                return;
            }
            try {
                serverTransaction.sendResponse(ringing);

            }
            catch (SipException ex) {
                call.setState(Call.DISCONNECTED);
                sipManCallback
                        .fireCommunicationsError(new CommunicationsException(
                                "Failed to send a RINGING response to an INVITE request!",
                                ex));
                return;
            } catch (InvalidArgumentException e) {
                call.setState(Call.DISCONNECTED);
                sipManCallback
                        .fireCommunicationsError(new CommunicationsException(
                                "Failed to send a NOT_FOUND response to an INVITE request!",
                                e));
                return;
            }
        } else {
            // Send BUSY_HERE
            Response busy = null;
            try {
                busy = sipManCallback.messageFactory.createResponse(
                        Response.BUSY_HERE, invite);
                sipManCallback.attachToTag(busy, dialog);
            }
            catch (ParseException ex) {
                sipManCallback
                        .fireCommunicationsError(new CommunicationsException(
                                "Failed to create a RINGING response to an INVITE request!",
                                ex));
                return;
            }
            try {
                serverTransaction.sendResponse(busy);
            }
            catch (SipException ex) {
                sipManCallback
                        .fireCommunicationsError(new CommunicationsException(
                                "Failed to send a RINGING response to an INVITE request!",
                                ex));
                return;
            } catch (InvalidArgumentException e) {
                sipManCallback
                        .fireCommunicationsError(new CommunicationsException(
                                "Failed to send a RINGING response to an INVITE request!",
                                e));
                return;
            }
        }
    }

    void processTimeout(Transaction transaction, Request request) {
        Call call = callDispatcher.findCall(transaction.getDialog());
        if (call == null) {
            return;
        }
        sipManCallback.fireCommunicationsError(new CommunicationsException(
                "The remote party has not replied!"
                        + "The call will be disconnected"));
        // change status
        call.setState(Call.DISCONNECTED);

    }

    void processBye(ServerTransaction serverTransaction, Request byeRequest) {
   	 
        try {
            // find the call
            Call call = callDispatcher.findCall(serverTransaction.getDialog());
            if (call == null) {
                Log.debug("No call find");
                sipManCallback.fireUnknownMessageReceived(byeRequest);
                return;
            }

            // Send OK
            Response ok = null;
            try {
                ok = sipManCallback.messageFactory.createResponse(Response.OK,
                        byeRequest);
                sipManCallback.attachToTag(ok, call.getDialog());
            }
            catch (ParseException ex) {
                sipManCallback
                        .fireCommunicationsError(new CommunicationsException(
                                "Failed to construct an OK response to a BYE request!",
                                ex));
                return;
            }
            try {
                serverTransaction.sendResponse(ok);
            }
            catch (SipException ex) {
                // This is not really a problem according to the RFC
                // so just dump to stdout should someone be interested
            }

            endCall(call.getID());

        }
        catch (Exception e) {
            Log.error("processBye", e);
        }
    }

    void processAck(ServerTransaction serverTransaction, Request ackRequest) {
        if (!serverTransaction.getDialog().getFirstTransaction()
                .getRequest().getMethod().equals(Request.INVITE)) {

            return;
        }
        // find the call
        Call call = callDispatcher.findCall(serverTransaction.getDialog());
        if (call == null) {
            // this is most probably the ack for a killed call - don't
            // signal it
            // sipManCallback.fireUnknownMessageReceived(ackRequest);

            return;
        }
        ContentLengthHeader cl = ackRequest.getContentLength();
        if (cl != null && cl.getContentLength() > 0) {
            call.setRemoteSdpDescription(new String(ackRequest
                    .getRawContent()));
        }
        // change status
        call.setState(Call.CONNECTED);
    }

    void processCancel(ServerTransaction serverTransaction,
                       Request cancelRequest) {
        if (!serverTransaction.getDialog().getFirstTransaction()
                .getRequest().getMethod().equals(Request.INVITE)) {
            // For someone else

            return;
        }
        // find the call
        Call call = callDispatcher.findCall(serverTransaction.getDialog());
        if (call == null) {
            sipManCallback.fireUnknownMessageReceived(cancelRequest);
            return;
        }
        // change status
        call.setState(Call.DISCONNECTED);
        // Cancels should be OK-ed and the initial transaction - terminated
        // (report and fix by Ranga)
        try {
            Response ok = sipManCallback.messageFactory.createResponse(
                    Response.OK, cancelRequest);
            sipManCallback.attachToTag(ok, call.getDialog());
            serverTransaction.sendResponse(ok);

        }
        catch (ParseException ex) {
            sipManCallback
                    .fireCommunicationsError(new CommunicationsException(
                            "Failed to create an OK Response to an CANCEL request.",
                            ex));
        }
        catch (SipException ex) {
            sipManCallback
                    .fireCommunicationsError(new CommunicationsException(
                            "Failed to send an OK Response to an CANCEL request.",
                            ex));
        } catch (InvalidArgumentException e) {
            sipManCallback
                    .fireCommunicationsError(new CommunicationsException(
                            "Failed to send a NOT_FOUND response to an INVITE request!",
                            e));

        }
        try {
            // stop the invite transaction as well
            Transaction tran = call.getDialog().getFirstTransaction();
            // should be server transaction and misplaced cancels should be
            // filtered by the stack but it doesn't hurt checking anyway
            if (!(tran instanceof ServerTransaction)) {
                sipManCallback
                        .fireCommunicationsError(new CommunicationsException(
                                "Received a misplaced CANCEL request!"));
                return;
            }
            ServerTransaction inviteTran = (ServerTransaction) tran;
            Request invite = call.getDialog().getFirstTransaction()
                    .getRequest();
            Response requestTerminated = sipManCallback.messageFactory
                    .createResponse(Response.REQUEST_TERMINATED, invite);
            sipManCallback.attachToTag(requestTerminated, call.getDialog());
            inviteTran.sendResponse(requestTerminated);

        }
        catch (ParseException ex) {
            sipManCallback
                    .fireCommunicationsError(new CommunicationsException(
                            "Failed to create a REQUEST_TERMINATED Response to an INVITE request.",
                            ex));
        }
        catch (SipException ex) {
            sipManCallback
                    .fireCommunicationsError(new CommunicationsException(
                            "Failed to send an REQUEST_TERMINATED Response to an INVITE request.",
                            ex));
        } catch (InvalidArgumentException e) {
            sipManCallback
                    .fireCommunicationsError(new CommunicationsException(
                            "Failed to send a NOT_FOUND response to an INVITE request!",
                            e));
        }
    }

    // ----------------- Responses --------------------------
    // NOT FOUND
    void processNotFound(ClientTransaction clientTransaction, Response response) {
        if (!clientTransaction.getDialog().getFirstTransaction()
                .getRequest().getMethod().equals(Request.INVITE)) {
            // Not for us

            return;
        }
        // find the call
        Call call = callDispatcher.findCall(clientTransaction.getDialog());
        if (call != null)
            call.setState(Call.DISCONNECTED);
        sipManCallback.fireCallRejectedRemotely(
                "Number NOT found at the server.", response, call);

    }

    void processNotImplemented(ClientTransaction clientTransaction,
                               Response response) {
        if (!clientTransaction.getDialog().getFirstTransaction()
                .getRequest().getMethod().equals(Request.INVITE)) {
            // Not for us

            return;
        }
        // find the call
        Call call = callDispatcher.findCall(clientTransaction.getDialog());
        call.setState(Call.DISCONNECTED);
        sipManCallback.fireCallRejectedRemotely(
                "Server cannot dial this number.", response, call);

    }

    // -------------------------------- User Initiated processing
    // ---------------------------------
    Call invite(String callee, String sdpContent)
            throws CommunicationsException {
        callee = callee.trim();
        // Remove excessive characters from phone numbers such as '
        // ','(',')','-'
        String excessiveChars = SIPConfig.getExcessiveURIChar();

        if (excessiveChars != null) {
            StringBuffer calleeBuff = new StringBuffer(callee);
            for (int i = 0; i < excessiveChars.length(); i++) {
                String charToDeleteStr = excessiveChars.substring(i, i + 1);

                int charIndex = -1;
                while ((charIndex = calleeBuff.indexOf(charToDeleteStr)) != -1)
                    calleeBuff.delete(charIndex, charIndex + 1);
            }
            callee = calleeBuff.toString();
        }

        // Handle default domain name (i.e. transform 1234 -> 1234@sip.com

        String defaultDomainName = SIPConfig.getDefaultDomain();
        if (defaultDomainName != null // no sip scheme
                && !callee.trim().startsWith("tel:")
                && callee.indexOf('@') == -1 // most probably a sip uri
                ) {
            callee = callee + "@" + defaultDomainName;
        }

        // Let's be uri fault tolerant
        if (callee.toLowerCase().indexOf("sip:") == -1 // no sip scheme
                && callee.indexOf('@') != -1 // most probably a sip uri
                ) {
            callee = "sip:" + callee;

        }
        // Request URI
        URI requestURI;
        try {
            requestURI = sipManCallback.addressFactory.createURI(callee);
        }
        catch (ParseException ex) {

            throw new CommunicationsException(callee
                    + " is not a legal SIP uri!", ex);
        }
        // Call ID
        CallIdHeader callIdHeader = sipManCallback.sipProvider
                .getNewCallId();
        // CSeq
        CSeqHeader cSeqHeader;
        try {
            cSeqHeader = sipManCallback.headerFactory.createCSeqHeader(1L,
                    Request.INVITE);
        }
        catch (ParseException ex) {
            // Shouldn't happen

            throw new CommunicationsException(
                    "An unexpected erro occurred while"
                            + "constructing the CSeqHeadder", ex);
        }
        catch (InvalidArgumentException ex) {
            // Shouldn't happen
            throw new CommunicationsException(
                    "An unexpected erro occurred while"
                            + "constructing the CSeqHeadder", ex);
        }
        // FromHeader
        FromHeader fromHeader = sipManCallback.getFromHeader();
        // ToHeader
        Address toAddress = sipManCallback.addressFactory
                .createAddress(requestURI);
        ToHeader toHeader;
        try {
            toHeader = sipManCallback.headerFactory.createToHeader(
                    toAddress, null);
        }
        catch (ParseException ex) {
            // Shouldn't happen
            throw new CommunicationsException(
                    "Null is not an allowed tag for the to header!", ex);
        }
        // ViaHeaders
        ArrayList<ViaHeader> viaHeaders = sipManCallback.getLocalViaHeaders();
        // MaxForwards
        MaxForwardsHeader maxForwards = sipManCallback
                .getMaxForwardsHeader();
        // Contact
        ContactHeader contactHeader = sipManCallback.getContactHeader();
        Request invite = null;
        try {
            invite = sipManCallback.messageFactory.createRequest(
                    requestURI, Request.INVITE, callIdHeader, cSeqHeader,
                    fromHeader, toHeader, viaHeaders, maxForwards);
        }
        catch (ParseException ex) {
            throw new CommunicationsException(
                    "Failed to create invite Request!", ex);
        }
        //
        invite.addHeader(contactHeader);

        AllowHeader allow = null;
        try {
            allow = sipManCallback.headerFactory.createAllowHeader("INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, NOTIFY, MESSAGE, SUBSCRIBE, INFO");
            invite.addHeader(allow);
        } catch (ParseException e) {
            Log.error(e);
        }

        // Content
        ContentTypeHeader contentTypeHeader = null;
        try {
            // content type should be application/sdp (not applications)
            // reported by Oleg Shevchenko (Miratech)
            contentTypeHeader = sipManCallback.headerFactory
                    .createContentTypeHeader("application", "sdp");
        }
        catch (ParseException ex) {
            // Shouldn't happen
            throw new CommunicationsException(
                    "Failed to create a content type header for the INVITE request",
                    ex);
        }
        try {
            invite.setContent(sdpContent, contentTypeHeader);
        }
        catch (ParseException ex) {
            throw new CommunicationsException(
                    "Failed to parse sdp data while creating invite request!",
                    ex);
        }
        // Transaction
        ClientTransaction inviteTransaction;
        try {
            inviteTransaction = sipManCallback.sipProvider
                    .getNewClientTransaction(invite);
        }
        catch (TransactionUnavailableException ex) {
            throw new CommunicationsException(
                    "Failed to create inviteTransaction.\n"
                            + "This is most probably a network connection error.",
                    ex);
        }
        try {
            inviteTransaction.sendRequest();
        }
        catch (SipException ex) {
            throw new CommunicationsException(
                    "An error occurred while sending invite request", ex);
        }
        Call call = callDispatcher.createCall(
                inviteTransaction.getDialog(), invite);
        call.setState(Call.DIALING);
        return call;
    }

    // Hold

    public void hold(int callID, String sdpContent)
            throws CommunicationsException {
        Call call = callDispatcher.getCall(callID);
        Request invite = null;

        invite = call.getDialog().getFirstTransaction().getRequest();

        try {
            invite = call.getDialog().createRequest(Request.INVITE);

        } catch (SipException e) {
            Log.error("hold", e);
        }

        long cseq = ((CSeq) (invite.getHeader(CSeq.NAME)))
                .getSequenceNumber() + 1;
        invite.removeHeader(CSeq.NAME);
        try {
            invite.addHeader(sipManCallback.headerFactory.createCSeqHeader(cseq, Request.INVITE));
        }
        catch (Exception e) {
            Log.error("hold", e);
        }

        invite.removeHeader(ViaHeader.NAME);

        for (ViaHeader via : sipManCallback.getLocalViaHeaders())
            invite.addHeader(via);

        ContentTypeHeader contentTypeHeader = null;
        try {
            // content type should be application/sdp (not applications)
            // reported by Oleg Shevchenko (Miratech)
            contentTypeHeader = sipManCallback.headerFactory
                    .createContentTypeHeader("application", "sdp");
        }
        catch (ParseException ex) {
            // Shouldn't happen
            throw new CommunicationsException(
                    "Failed to create a content type header for the INVITE request",
                    ex);
        }
        try {
            invite.setContent(sdpContent, contentTypeHeader);
        }
        catch (ParseException ex) {
            throw new CommunicationsException(
                    "Failed to parse sdp data while creating invite request!",
                    ex);
        }

        // Transaction
        ClientTransaction inviteTransaction;
        try {

            inviteTransaction = sipManCallback.sipProvider.getNewClientTransaction(invite);

            call.getDialog().sendRequest(inviteTransaction);

            call.setLastRequest(invite);
        }
        catch (SipException ee) {
            Log.error("hold", ee);
        }

        return;
    }

    // send DTMF
    void sendDTMF(int callID, String digit) {
        try {
            Call call = callDispatcher.getCall(callID);

            Dialog dialog = call.getDialog();
            sendNumDTMF(dialog, digit);
            //
        }
        catch (CommunicationsException e) {

        }

    } // send DTMF

    // end call
    void endCall(int callID) throws CommunicationsException {
        Call call = callDispatcher.getCall(callID);
        if (call == null) {
            throw new CommunicationsException(
                    "Could not find call with id=" + callID);
        } else {
            endCall(call);
        }

    } // end call

    void endCall(Call call) throws CommunicationsException {
        if (call == null) {
            throw new CommunicationsException(
                    "Could not find call");
        }
        Dialog dialog = call.getDialog();
        if (call.getState().equals(Call.CONNECTED)
                || call.getState().equals(Call.RECONNECTED)) {
            call.setState(Call.DISCONNECTED);
            sayBye(dialog);
        } else if (call.getState().equals(Call.DIALING)
                || call.getState().equals(Call.RINGING)) {
            if (dialog.getFirstTransaction() != null) {
                try {
                    // Someone knows about us. Let's be polite and say we
                    // are leaving
                    sayCancel(dialog);
                }
                catch (CommunicationsException ex) {
                    // something went wrong let's just tell the others
                    sipManCallback
                            .fireCommunicationsError(new CommunicationsException(
                                    "Could not send the CANCEL request! "
                                            + "Remote party won't know we're leaving!",
                                    ex));
                }
            }
            call.setState(Call.DISCONNECTED);
        } else if (call.getState().equals(Call.ALERTING)) {
            call.setState(Call.DISCONNECTED);
            sayBusyHere(dialog);
        }
        // For FAILED and BUSY we only need to update CALL_STATUS
        else if (call.getState().equals(Call.BUSY)) {
            call.setState(Call.DISCONNECTED);
        } else if (call.getState().equals(Call.FAILED)) {
            call.setState(Call.DISCONNECTED);
        } else {
            call.setState(Call.DISCONNECTED);
            throw new CommunicationsException(
                    "Could not determine call state!");
        }
    } // end call

    // send dtmf

    private void sendNumDTMF(Dialog dialog, String digit)
            throws CommunicationsException {

        Request info = null;
        String body = "Signal=" + digit + "\nDuration=160";
        String contentType = "application/dtmf-relay";
        String[] contentTypeTab = contentType.split("/");
        ContentTypeHeader contentTypeHeader = null;
        try {
            info = dialog.createRequest(Request.INFO);
            try {
                contentTypeHeader = sipManCallback.headerFactory
                        .createContentTypeHeader(contentTypeTab[0],
                                contentTypeTab[1]);
                info.setContent(body, contentTypeHeader);
            }
            catch (ParseException ex) {
                throw new CommunicationsException(
                        "ContentType Header must look like type/subtype!",
                        ex);
            }

        }
        catch (SipException ex) {
            throw new CommunicationsException(
                    "Failed to create bye request!", ex);
        }
        ClientTransaction clientTransaction = null;
        try {
            clientTransaction = sipManCallback.sipProvider
                    .getNewClientTransaction(info);
        }
        catch (TransactionUnavailableException ex) {
            throw new CommunicationsException(
                    "Failed to construct a client transaction from the INFO request",
                    ex);
        }
        try {
            dialog.sendRequest(clientTransaction);

        }
        catch (SipException ex1) {
            throw new CommunicationsException(
                    "Failed to send the INFO request");
        }
    }

    // send message
//    private void sendInfoMessage(Dialog dialog, String body)
//            throws CommunicationsException {
//        Request request = dialog.getFirstTransaction().getRequest();
//        Request info = null;
//        String contentType = "application/dtmd-relay";
//        String[] contentTypeTab = contentType.split("/");
//        ContentTypeHeader contentTypeHeader = null;
//        try {
//            info = dialog.createRequest(Request.INFO);
//            try {
//                contentTypeHeader = sipManCallback.headerFactory
//                        .createContentTypeHeader(contentTypeTab[0],
//                                contentTypeTab[1]);
//                info.setContent(body, contentTypeHeader);
//            }
//            catch (ParseException ex) {
//
//                throw new CommunicationsException(
//                        "ContentType Header must look like type/subtype!",
//                        ex);
//            }
//        }
//        catch (SipException ex) {
//
//            throw new CommunicationsException(
//                    "Failed to create bye request!", ex);
//        }
//        ClientTransaction clientTransaction = null;
//        try {
//            clientTransaction = sipManCallback.sipProvider
//                    .getNewClientTransaction(info);
//        }
//        catch (TransactionUnavailableException ex) {
//
//            throw new CommunicationsException(
//                    "Failed to construct a client transaction from the INFO request",
//                    ex);
//        }
//        try {
//            dialog.sendRequest(clientTransaction);
//        }
//        catch (SipException ex1) {
//            throw new CommunicationsException(
//                    "Failed to send the INFO request");
//        }
//
//    } // send message

    // Bye
    private void sayBye(Dialog dialog) throws CommunicationsException {
        Request bye = null;

        bye = dialog.getFirstTransaction().getRequest();

        try {
            bye = dialog.createRequest(Request.BYE);

        } catch (SipException e) {
            Log.error("bye", e);
        }

        long cseq = ((CSeq) (bye.getHeader(CSeq.NAME)))
                .getSequenceNumber() + 1;
        bye.removeHeader(CSeq.NAME);
        try {
            bye.addHeader(sipManCallback.headerFactory.createCSeqHeader(cseq, Request.BYE));
        }
        catch (Exception e) {
            Log.error("bye", e);
        }

        bye.removeHeader(ViaHeader.NAME);

        for (ViaHeader via : sipManCallback.getLocalViaHeaders())
            bye.addHeader(via);

        ContactHeader contactHeader = sipManCallback.getContactHeader();
        bye.addHeader(contactHeader);

        AllowHeader allow = null;
        try {
            allow = sipManCallback.headerFactory.createAllowHeader("INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, NOTIFY, MESSAGE, SUBSCRIBE, INFO");
            bye.addHeader(allow);
        } catch (ParseException e) {
            Log.error(e);
        }

        // Transaction
        ClientTransaction inviteTransaction;
        try {

            inviteTransaction = sipManCallback.sipProvider.getNewClientTransaction(bye);

            dialog.sendRequest(inviteTransaction);

        }
        catch (SipException ee) {
            Log.error("bye", ee);
        }

        return;
    } // bye

    // cancel
    private void sayCancel(Dialog dialog) throws CommunicationsException {
        if (dialog.isServer()) {

            throw new CommunicationsException(
                    "Cannot cancel a server transaction");
        }
        ClientTransaction clientTransaction = (ClientTransaction) dialog
                .getFirstTransaction();
        try {
            Request cancel = clientTransaction.createCancel();
            ClientTransaction cancelTransaction = sipManCallback.sipProvider
                    .getNewClientTransaction(cancel);
            cancelTransaction.sendRequest();
        }
        catch (SipException ex) {

            throw new CommunicationsException(
                    "Failed to send the CANCEL request", ex);
        }
    } // cancel

    // busy here
    private void sayBusyHere(Dialog dialog) throws CommunicationsException {
        Request request = dialog.getFirstTransaction().getRequest();
        Response busyHere = null;
        try {
            busyHere = sipManCallback.messageFactory.createResponse(
                    Response.BUSY_HERE, request);
            sipManCallback.attachToTag(busyHere, dialog);
        }
        catch (ParseException ex) {

            throw new CommunicationsException(
                    "Failed to create the BUSY_HERE response!", ex);
        }
        if (!dialog.isServer()) {

            throw new CommunicationsException(
                    "Cannot send BUSY_HERE in a client transaction");
        }
        ServerTransaction serverTransaction = (ServerTransaction) dialog
                .getFirstTransaction();
        try {
            serverTransaction.sendResponse(busyHere);

        }
        catch (SipException ex) {

            throw new CommunicationsException(
                    "Failed to send the BUSY_HERE response", ex);
        } catch (InvalidArgumentException e) {
            throw new CommunicationsException(
                    "Failed to send the BUSY_HERE response", e);
        }

    } // busy here

    // ------------------ say ok
    public void sayOK(int callID, String sdpContent)
            throws CommunicationsException {
        Call call = callDispatcher.getCall(callID);
        if (call == null) {
            throw new CommunicationsException(
                    "Failed to find call with id=" + callID);
        }

        if (!call.isIncoming()) return;

        Dialog dialog = call.getDialog();
        if (dialog == null) {
            call.setState(Call.DISCONNECTED);
            throw new CommunicationsException(
                    "Failed to extract call's associated dialog! Ending Call!");
        }
        Transaction transaction = dialog.getFirstTransaction();
        if (transaction == null || !dialog.isServer()) {
            call.setState(Call.DISCONNECTED);
            throw new CommunicationsException(
                    "Failed to extract a ServerTransaction "
                            + "from the call's associated dialog!");
        }
        ServerTransaction serverTransaction = (ServerTransaction) transaction;
        Response ok = null;
        try {
            ok = sipManCallback.messageFactory.createResponse(Response.OK,
                    dialog.getFirstTransaction().getRequest());
            sipManCallback.attachToTag(ok, dialog);
        }
        catch (ParseException ex) {
            call.setState(Call.DISCONNECTED);
            throw new CommunicationsException(
                    "Failed to construct an OK response to an INVITE request",
                    ex);
        }
        // Content
        ContentTypeHeader contentTypeHeader = null;
        try {
            // content type should be application/sdp (not applications)
            // reported by Oleg Shevchenko (Miratech)
            contentTypeHeader = sipManCallback.headerFactory
                    .createContentTypeHeader("application", "sdp");
        }
        catch (ParseException ex) {
            // Shouldn't happen
            call.setState(Call.DISCONNECTED);
            throw new CommunicationsException(
                    "Failed to create a content type header for the OK request",
                    ex);
        }
        try {
            ok.setContent(sdpContent, contentTypeHeader);
        }
        catch (NullPointerException ex) {
            call.setState(Call.DISCONNECTED);
            throw new CommunicationsException(
                    "No sdp data was provided for the ok response to an INVITE request!",
                    ex);
        }
        catch (ParseException ex) {
            call.setState(Call.DISCONNECTED);
            throw new CommunicationsException(
                    "Failed to parse sdp data while creating invite request!",
                    ex);
        }
        // TODO This is here provisionally as my remote user agent that I am
        // using for
        // testing is not doing it. It is not correct from the protocol
        // point of view
        // and should probably be removed
        if (((ToHeader) ok.getHeader(ToHeader.NAME)).getTag() == null) {
            try {
                ((ToHeader) ok.getHeader(ToHeader.NAME)).setTag(Integer
                        .toString(dialog.hashCode()));
            }
            catch (ParseException ex) {
                call.setState(Call.DISCONNECTED);
                throw new CommunicationsException("Unable to set to tag",
                        ex);
            }
        }
        ContactHeader contactHeader = sipManCallback.getContactHeader();
        ok.addHeader(contactHeader);
        try {
            serverTransaction.sendResponse(ok);

        }
        catch (SipException ex) {
            call.setState(Call.DISCONNECTED);

            throw new CommunicationsException(
                    "Failed to send an OK response to an INVITE request",
                    ex);
        } catch (InvalidArgumentException e) {
            call.setState(Call.DISCONNECTED);
            sipManCallback
                    .fireCommunicationsError(new CommunicationsException(
                            "Failed to send a NOT_FOUND response to an INVITE request!",
                            e));
        }

    } // answer call

    // ------------------ Internal Error
    void sayInternalError(int callID) throws CommunicationsException {
        Call call = callDispatcher.getCall(callID);
        if (call == null) {

            throw new CommunicationsException(
                    "Failed to find call with id=" + callID);
        }
        Dialog dialog = call.getDialog();
        if (dialog == null) {
            call.setState(Call.DISCONNECTED);
            throw new CommunicationsException(
                    "Failed to extract call's associated dialog! Ending Call!");
        }
        Transaction transaction = dialog.getFirstTransaction();
        if (transaction == null || !dialog.isServer()) {
            call.setState(Call.DISCONNECTED);
            throw new CommunicationsException(
                    "Failed to extract a transaction from the call's associated dialog!");
        }
        ServerTransaction serverTransaction = (ServerTransaction) transaction;
        Response internalError = null;
        try {
            internalError = sipManCallback.messageFactory.createResponse(
                    Response.SERVER_INTERNAL_ERROR, dialog
                    .getFirstTransaction().getRequest());
            sipManCallback.attachToTag(internalError, dialog);
        }
        catch (ParseException ex) {
            call.setState(Call.DISCONNECTED);
            throw new CommunicationsException(
                    "Failed to construct an OK response to an INVITE request",
                    ex);
        }
        ContactHeader contactHeader = sipManCallback.getContactHeader();
        internalError.addHeader(contactHeader);
        try {
            serverTransaction.sendResponse(internalError);

        }
        catch (SipException ex) {
            call.setState(Call.DISCONNECTED);
            throw new CommunicationsException(
                    "Failed to send an OK response to an INVITE request",
                    ex);
        } catch (InvalidArgumentException e) {
            call.setState(Call.DISCONNECTED);
            throw new CommunicationsException(
                    "Failed to send an OK response to an INVITE request",
                    e);
        }

    } // internal error

    /**
     * @return Returns the callDispatcher.
     * @uml.property name="callDispatcher"
     */
    CallDispatcher getCallDispatcher() {
        return callDispatcher;
    }

    // The following method is currently being implemented and tested
    protected void processReInvite(ServerTransaction serverTransaction,
                                   Request invite) {
        sipManCallback.setBusy(false);

        Log.debug("REINVITE DETECTED");

        Call call = callDispatcher.findCall(serverTransaction.getDialog());
        if (call == null) {
            call = callDispatcher.createCall(serverTransaction.getDialog(),
                    invite);
        }

        call.setRemoteSdpDescription(new String(invite.getRawContent()));

        Log.debug("CALL CONNECT EVENT");

        try {
            sayOK(call.getID(), call.getLocalSdpDescription().toString());
        } catch (CommunicationsException e) {
            Log.error("Re-Invite", e);
        }

        call.setState(Call.MOVING_REMOTELY);
        call.setState(Call.CONNECTED);

    }
}
