/*
 * Copyright 2007 Sun Microsystems, Inc.
 *
 * This file is part of jVoiceBridge.
 *
 * jVoiceBridge is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation and distributed hereunder
 * to you.
 *
 * jVoiceBridge 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, see <http://www.gnu.org/licenses/>.
 *
 * Sun designates this particular file as subject to the "Classpath"
 * exception as provided by Sun in the License file that accompanied this
 * code.
 */

package com.sun.voip.server;

import com.sun.voip.CallEvent;
import com.sun.voip.CallState;
import com.sun.voip.Logger;
import com.sun.voip.MediaInfo;
import com.sun.voip.SdpInfo;
import com.sun.voip.TreatmentManager;

import javax.sip.*;
import javax.sip.header.*;
import javax.sip.message.*;
import javax.sip.address.*;
import javax.sip.InvalidArgumentException;

import java.net.InetAddress;
import java.net.InetSocketAddress;

import java.io.IOException;

import java.text.ParseException;

import java.util.Vector;

/**
 * Third Party Call Control SIP User Agent.
 *
 *    SipTPCCallAgent	     VoIPGateway or SIP proxy
 *
 *        |(1) INVITE with SDP    |
 *        |---------------------->|
 *        |(2) 200 OK with SDP	  |
 *        |<----------------------|
 *        |(3) ACK with SDP	  |
 *	  |---------------------->|
 *        |(4) treatment then RTP |
 *        |......................>|
 */
public class SipTPCCallAgent extends CallSetupAgent implements SipListener {

    private SipServer.SipServerCallback sipServerCallback;

    private SipUtil sipUtil;

    private ClientTransaction clientTransaction;  // most recent client trans
    private String sdpBody;
    private String sipCallId;
    private boolean receivedBye;

    private boolean callAnswered = false;

    private static boolean forceGatewayError = false;

    private boolean ackSent = false;

    private boolean gotOk;

    private TreatmentManager busyTreatment;

    /**
     * Constructor
     * @param callHandler CallHandler this CallParticipant is associated with
     */
    public SipTPCCallAgent(CallHandler callHandler) {
	super(callHandler);

	MediaInfo mixerMediaPreference = callHandler.getConferenceManager().getMediaInfo();

	sipUtil = new SipUtil(mixerMediaPreference);
    }

    public static void forceGatewayError(boolean forceGatewayError) {
	SipTPCCallAgent.forceGatewayError = forceGatewayError;
    }

    /*
     * Begin Third-Party Call Control.
     */
    public void initiateCall() throws IOException {
        try {
	    try {
                busyTreatment = new TreatmentManager("busy.au", 0);
	    } catch (IOException e) {
		Logger.println("Invalid busy treatment:  " + e.getMessage());
	    }

            Logger.writeFile("Call " + cp
		+ ":   Begin SIP third party call");

            setState(CallState.INVITED);

	    InetSocketAddress isa = callHandler.getReceiveAddress();

	    if (isa == null) {
		throw new IOException("can't get receiver socket!");
	    }

            // send INVITE to the CallParticipant
            clientTransaction = sipUtil.sendInvite(cp, isa);

	    if (clientTransaction == null) {
                Logger.error("Error placing call:  " + cp);
		setState(CallState.ENDED, "Reason='Error placing call'");
		throw new IOException("Error placing call:  " + cp);
	    }

            CallIdHeader callIdHeader = (CallIdHeader)
                clientTransaction.getRequest().getHeader(CallIdHeader.NAME);

            sipCallId = callIdHeader.getCallId();

            sipServerCallback = SipServer.getSipServerCallback();
	    sipServerCallback.addSipListener(sipCallId, this);
	} catch (java.text.ParseException e) {
            Logger.println("Call " + cp + " Error placing call " + cp +":  "
		+  e.getMessage());
	    setState(CallState.ENDED, "Reason='Error placing call " + cp + " "
		+ e.getMessage() + "'");
	    throw new IOException("Error placing call " + cp + " "
		+ e.getMessage());
	} catch (InvalidArgumentException e) {
            Logger.println("Call " + cp + " Error placing call " + cp +":  "
		+  e.getMessage());
	    setState(CallState.ENDED, "Reason='Error placing call " + cp + " "
		+ e.getMessage() + "'");
	    throw new IOException("Error placing call " + cp + " "
		+ e.getMessage());
	} catch (SipException e) {
            Logger.println("Call " + cp + " Error placing call " + cp +":  "
		+  e.getMessage());
	    setState(CallState.ENDED, "Reason='Error placing call " + cp + " "
		+ e.getMessage() + "'");
	    throw new IOException("Error placing call " + cp + " "
		+ e.getMessage());
	}
    }

    /**
     * Done with treatments for call end, now terminate the call
     */
    public void terminateCall() {
	if (receivedBye == false) {
	    if (gotOk == false ||
		    (getState() == CallState.INVITED && callAnswered == false)) {

	        try {
		    Logger.writeFile("Call " + cp + ":  sendCancel");
                    sipUtil.sendCancel(clientTransaction);
	        } catch (Exception e) {
		    Logger.println("sendCancel " + e.getMessage());
	        }
	    } else {
		/*
		 * Try sending a BYE as well.
		 * Seems that when we treat SESSION_PROGRESS
		 * as OK, sometimes we need to send a CANCEL
		 * and other times a BYE.  We'll send both.
		 */
	        try {
		    Logger.writeFile("Call " + cp + ":  sendBye");
	            sipUtil.sendBye(clientTransaction);
	        } catch (Exception e) {
		    Logger.println("Call " + cp + ":  sendBye"
			+ e.getMessage());
	        }
	    }
	}
    }

    /**
     * Processes SIP requests.  The only request being handled is BYE.
     * @param requestReceivedEvent the event containing the SIP request
     */
    public synchronized void processRequest(RequestEvent requestReceivedEvent) {

        // obtain request and transaction id
        Request request = requestReceivedEvent.getRequest();

        ServerTransaction st = requestReceivedEvent.getServerTransaction();

        if (request.getMethod().equals(Request.BYE)) {
            handleBye(request, st);
        } else if (request.getMethod().equals(Request.INVITE)) {
	    /*
	     * This is a re-Invite
	     */
	    handleReInvite(request, st);
        } else if (request.getMethod().equals(Request.ACK)) {
	    Logger.println("Call " + cp + " got ACK");
        } else {
            // no other requests should come in other than BYE, INVITE or ACK
            Logger.writeFile("Call " + cp
		+ " ignoring request " + request.getMethod());
        }
    }

    /**
     * handles a BYE request
     * @param request the request
     * @param transId the transaction Id
     * @throws TransactionDoesNotExistException when the transaction
     *         record does not exist.
     */
    private void handleBye(Request request, ServerTransaction st) {
        try {
            CallIdHeader callIdHeader = (CallIdHeader)
		request.getHeader("Call-Id");

            String sipCallId = callIdHeader.getCallId();

            if (sipCallId.equals(this.sipCallId)) {
		receivedBye = true;

		try {
		    Logger.writeFile("Call " + cp + " has hung up.");

		    //sipUtil.sendOK(clientTransaction, st, cp);
		    sipUtil.sendOK(request, st);
		} catch (Exception e) {
		    /*
		     * We sometimes get a null ServerTransaction
		     */
		}
		cancelRequest("hung up");
		sipServerCallback.removeSipListener(sipCallId);
            } else {
                /*
		 * this should not happen since the message has been
                 * delegated to this sip agent.
                 */
                throw new TransactionDoesNotExistException(
                    cp + "BYE request received did not "
                    + "match either party:  " + request);
            }
        } catch (TransactionDoesNotExistException e) {
            Logger.error("Call " + cp
		+ " Transaction not found " + e.getMessage());
        } catch (SipException e) {
            Logger.exception("Call " + cp + " SIP Stack error", e);
	    cancelRequest("handleBye:  SIP Stack error " + e.getMessage());
        } catch (Exception e) {
            Logger.exception("Call " + cp + " Unknown error ", e);
	    cancelRequest("handleBye:  SIP Stack error " + e.getMessage());
        }
    }

    private void handleReInvite(Request request, ServerTransaction st) {
	Logger.println("Call " + cp + " Re-INVITE\n" + request);

        if (request.getRawContent() == null) {
            Logger.error("Call " + cp + " no SDP in INVITE Request!");
            return;
        }

        String sdpBody = new String(request.getRawContent());

	SdpInfo sdpInfo;

	try {
	    sdpInfo = sipUtil.getSdpInfo(sdpBody);
	} catch (ParseException e) {
	    Logger.error("Call " + cp + " invalid SDP in re-INVITE Request! "
		+ e.getMessage());
	    return;
	}

	MediaInfo mediaInfo = sdpInfo.getMediaInfo();

        InetSocketAddress isa = new InetSocketAddress(
	    sdpInfo.getRemoteHost(), sdpInfo.getRemotePort());

	InetSocketAddress rtcpAddress = sdpInfo.getRtcpAddress();

        setEndpointAddress(isa, mediaInfo.getPayload(),
	    sdpInfo.getTransmitMediaInfo().getPayload(),
	    sdpInfo.getTelephoneEventPayload(), rtcpAddress);

        isa = callHandler.getReceiveAddress();

        try {
            sipUtil.sendOkWithSdp(request, st, isa, sdpInfo);
        } catch (Exception e) {
            Logger.println("Call " + cp +
		" Failed to send ok with sdp for re-invite " + e.getMessage());
            return;
        }
    }

    /**
     * Processes SIP responses.
     * @param responseReceivedEvent the event containing the SIP response
     *
     * Note:  This method is called by a thread from the SIP stack.
     *        If it blocks, no other events will be delivered!
     */
    public static boolean rejectCall = false;	// for debugging!

    public synchronized void processResponse(
	    ResponseEvent responseReceivedEvent) {

        try {
            Response response = (Response)responseReceivedEvent.getResponse();
            ClientTransaction clientTransaction =
		responseReceivedEvent.getClientTransaction();

            int statusCode = response.getStatusCode();

	    FromHeader fromHeader = (FromHeader)
		response.getHeader(FromHeader.NAME);

            String displayName = fromHeader.getAddress().getDisplayName();

	    if (Logger.logLevel >= Logger.LOG_SIP) {
	        Logger.println("Response:  statusCode "
	    	    + statusCode + " state " + getCallState()
		    + " fromHeader " + displayName + " call participant "
		    + cp.getName());
	    }

	    if (reasonCallTerminated != null) {
		/*
		 * Ignore OK and Request Terminated.
		 * XXX what's the symbol for 487?
		 */
		if (statusCode != Response.OK && statusCode != 487) {
		    if (Logger.logLevel >= Logger.LOG_SIP) {
		        Logger.println("Call " + cp
			    + ":  request cancelled, ignoring response");
		    }
		}

                CallIdHeader callIdHeader = (CallIdHeader)
		    response.getHeader("Call-Id");

                String sipCallId = callIdHeader.getCallId();
		sipServerCallback.removeSipListener(sipCallId);
		return;
	    }

            /*
	     * Some type of global failure that prevents the
             * CallParticipant from being contacted, report failure.
             */
	    if (forceGatewayError) {
		statusCode = 500;
		forceGatewayError = false;
	    }

            if (statusCode >= 500 && getState() == CallState.INVITED) {
                Logger.error("Call " + cp + " gateway error:  " + statusCode
		    + " " + response.getReasonPhrase());
		cancelRequest("gateway error: " + statusCode + " "   + response.getReasonPhrase());
                return;

			} else if (statusCode == Response.PROXY_AUTHENTICATION_REQUIRED || statusCode == Response.UNAUTHORIZED) {

				if (cp.getProxyCredentials() != null)
				{
					try {
						SipServer.handleChallenge(response, clientTransaction, cp.getProxyCredentials()).sendRequest();

					} catch (Exception e) {

						Logger.println("Proxy authentification failed  " + e);
					}
				}
				return;


            } else if (statusCode >= 400) {

                // if we get a busy or an unknown error, play busy.
                Logger.println("Call " + cp  + " got status code :" + statusCode);

				cp.setCallEndTreatment(null);
				cp.setConferenceJoinTreatment(null);
				cp.setConferenceLeaveTreatment(null);

                /*
				 * play busy treatment, but deallocate any resources
                 * held up by ringBack first, if any.
                 */
                //stopCallAnsweredTreatment();

			if (statusCode == Response.BUSY_HERE) {
		    try {
			if (busyTreatment != null) {
			    addTreatment(busyTreatment);
                            //busyTreatment.waitForTreatment();
			} else {
			    Logger.println("Unable to play busy treatment!!!");
			}
		    } catch (Exception e) {
		        Logger.error("can't start busy treatment!" + sdpBody);
		    }

		    CallEvent callEvent = new CallEvent(CallEvent.BUSY_HERE);

		    callEvent.setInfo(response.getReasonPhrase());

		    sendCallEventNotification(callEvent);
		}

		//sipUtil.sendBye(clientTransaction);
                cancelRequest(response.getReasonPhrase());
                return;
            }

            /* state machine */
            switch (getState()) {
            /*
	     * CallParticipant picked up, send treatment if any,
	     * and wait for it to finish.
	     */
            case CallState.INVITED:
		if (rejectCall) {
                    Logger.error("Call " + cp + " gateway error:  "
			+ statusCode + " " + response.getReasonPhrase());

		    cancelRequest("gateway error: " + statusCode + " "
		        + response.getReasonPhrase());
                    return;
		}

                handleCallParticipantInvited(response, clientTransaction);
                break;

	    /*
	     * Call established, the ACK needs to be resent.
	     * According to Ranga, this is done by the NIST SIP Stack.
	     */
	    case CallState.ESTABLISHED:
		if (statusCode == Response.OK) {
		    gotOk = true;

                    Logger.writeFile("Call " + cp + " Got OK, ESTABLISHED");

		    if (ackSent == false) {
                        sipUtil.sendAck(clientTransaction);
		        ackSent = true;
		    }
		}
		break;

	    case CallState.ENDED:
		break;		// ignore the response

            default:
		Logger.error("Process Response bad state " + getState()
		    + "\n" + response);
            }
        } catch (SipException e) {
            Logger.exception("Call " + cp + " SIP Stack error ", e);

	    cancelRequest(
		"processResponse:  SIP Stack error " + e.getMessage());
        } catch (Exception e) {
            Logger.exception("processResponse:  " + cp, e);

	    cancelRequest(
		"processResponse:  SIP Stack error " + e.getMessage());
        }
    }

    /**
     * handles the INVITED state.
     * @param response the response
     * @param clientTransaction the client transaction
     * @throws SipException SIP stack related error
     */
    private void handleCallParticipantInvited(Response response,  ClientTransaction clientTransaction) throws ParseException, SipException,    InvalidArgumentException
    {
        FromHeader fromHeader = (FromHeader)
	    response.getHeader(FromHeader.NAME);

		String displayName = fromHeader.getAddress().getDisplayName();

        int statusCode = response.getStatusCode();

		Logger.println("handleCallParticipantInvited " + cp	+ " status " + statusCode + " " + response.getReasonPhrase());
		Logger.println("handleCallParticipantInvited , displayname " + displayName);

		CallIdHeader callIdHeader = (CallIdHeader) response.getHeader(CallIdHeader.NAME);

        if (sipCallId.equals(callIdHeader.getCallId()) &&
        	displayName.equals(cp.getDisplayName()) &&
        	(statusCode == Response.OK || statusCode == Response.SESSION_PROGRESS) &&
        	((CSeqHeader)response.getHeader(CSeqHeader.NAME)).getMethod().equals(Request.INVITE))
        {
	    	if (statusCode == Response.SESSION_PROGRESS) {
				/*
				* For some calls, we never get an OK.  Instead we just get
				* SESSION_PROGRESS.  In order to handle these calls, we treat
				* SESSION_PROGRESS as OK.  If an OK arrives later, we'll
				* send an ACK.  This flag allows us to enable or
				* disable this workaround for each call.
				*
				* The problem with always treating SESSION_PROGRESS as OK
				* is that in a conference everybody will hear the ringing sound
				* which the remote call sends until the call is actually answered.
				* This can be avoided if joinConfirmation is specified.
				* The other problem is that if we treat SESSION_PROGRESS
				* as though the call has been answered, then we'll start
				* playing the treatment before a person really answers to
				* hear the treatment.
				*/

				if (cp.getHandleSessionProgress() == false) {
					Logger.writeFile("Call " + cp + " Ignoring SESSION_PROGRESS");
					return;
				}

				Logger.writeFile("Call " + cp + " Treating SESSION_PROGRESS as OK");
			}

			if (response.getRawContent() == null)
			{
				Logger.error("Call " + cp + " no SDP in OK Response!");
				cancelRequest("SIP error!  no SDP in OK Response!");
				return;
			}

			this.clientTransaction = clientTransaction;

			if (statusCode == Response.OK)
			{
				gotOk = true;
				Logger.writeFile("Call " + cp + " Got OK, call answered\n" + response);
			}

			ToHeader toHeader = (ToHeader)response.getHeader(ToHeader.NAME);

				/*
			 * We got an OK response.
			 *
			 * send an ACK back to the CallParticipant
				 */

			if (statusCode == Response.OK) {
				sipUtil.sendAck(clientTransaction);
				ackSent = true;
			}

			if (callAnswered) {
				Logger.writeFile("Call " + cp + " done processing OK");
				return;
			}

			/*
			 * Remember the IP and port of where to send data to
			 * the CallParticipant.
			 */

			sdpBody = new String(response.getRawContent());

			SdpInfo sdpInfo;

			try {
				sdpInfo = sipUtil.getSdpInfo(sdpBody, false);

			} catch (ParseException e) {

				Logger.error("Call " + cp + " Invalid SDP in OK Response! "	+ e.getMessage());
				cancelRequest("SIP error!  Invalid SDP in OK Response!");
				return;
			}

			MediaInfo mediaInfo = sdpInfo.getMediaInfo();
			InetSocketAddress isa = new InetSocketAddress(sdpInfo.getRemoteHost(), sdpInfo.getRemotePort());
			InetSocketAddress rtcpAddress = sdpInfo.getRtcpAddress();
			setEndpointAddress(isa, mediaInfo.getPayload(),sdpInfo.getTransmitMediaInfo().getPayload(),sdpInfo.getTelephoneEventPayload(), rtcpAddress);

			/*
			 * The CallParticipant has answered.
			 * If join confirmation is required, we remain in the
			 * INVITED state.  We set the callAnswered flag so that
			 * if the join confirmation times out we know to
			 * send a BYE rather than a CANCEL.
			 */

			callAnswered = true;

			if (cp.getJoinConfirmationTimeout() == 0) {
				setState(CallState.ANSWERED);
			}

			/*
			 * Start treatment if any and wait for it to finish.
			 * When the treatment finishes, notification will
			 * be delivered to our parent which will indicate
			 * we're ready for the conference.
			 *
			 * If there's no treatment to be played, we're ready now
			 * unless we're waiting for join confirmation..
			 */
			initializeCallAnsweredTreatment();

			if (callAnsweredTreatment != null) {
				startCallAnsweredTreatment();
			} else {

				if (cp.getJoinConfirmationTimeout() == 0) {
						setState(CallState.ESTABLISHED);
				}
			}

        } else {
            Logger.writeFile("Call " + cp + " Ignoring response: " + response.getReasonPhrase());

			if (Logger.logLevel >= Logger.LOG_SIP) {
					Logger.println("Call " + cp + " Response: " + response);
			}
		}
    }

    /**
     * Processes a retransmit or expiration Timeout of an underlying
     * {@link Transaction} handled by this SipListener. This Event notifies the
     * application that a retransmission or transaction Timer expired in the
     * SipProvider's transaction state machine. The TimeoutEvent encapsulates
     * the specific timeout type and the transaction identifier either client
     * or server upon which the timeout occured. The type of Timeout can by
     * determined by:
     * <code>timeoutType = timeoutEvent.getTimeout().getValue();</code>
     *
     * @param timeoutEvent - the timeoutEvent received indicating either the
     * message retransmit or transaction timed out.
     */
    public void processTimeout(TimeoutEvent timeoutEvent) {
    }

    public void processDialogTerminated(DialogTerminatedEvent dte) {
        if (Logger.logLevel >= Logger.LOG_SIP) {
            Logger.println("processDialogTerminated called");
	}
    }

    public void  processTransactionTerminated(TransactionTerminatedEvent tte) {
        if (Logger.logLevel >= Logger.LOG_SIP) {
            Logger.println("processTransactionTerminated called");
	}
    }

    public void  processIOException(IOExceptionEvent ioee) {
        if (Logger.logLevel >= Logger.LOG_SIP) {
            Logger.println("processTransactionTerminated called");
	}
    }

}
