/*
 * 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.CallParticipant;
import com.sun.voip.CallState;
import com.sun.voip.CallEvent;
import com.sun.voip.CallEventListener;
import com.sun.voip.Logger;
import com.sun.voip.TreatmentManager;

import java.io.IOException;

import java.net.InetSocketAddress;
import java.net.SocketException;

import java.util.ArrayList;
import org.voicebridge.Application;

/**
 * Initiate a call, and join a conference
 *
 * This is a separate thread so that it monitor the call status.
 *
 * This class handles a	single party (joining a conference)
 * as well as two party calls.
 *
 * There is also support to try alternate gateways if one
 * gateway can't handle a new call.
 */
public class OutgoingCallHandler extends CallHandler implements CallEventListener {

    private CallEventListener csl;

    private Integer callInitiatedLock = new Integer(0);
    private Integer stateChangeLock = new Integer(0);
    private Integer waitCallAnswerLock = new Integer(0);
    private Integer waitCallEstablishedLock = new Integer(0);

    private boolean lastGateway = false;
    private boolean onlyOneGateway = false;

    public OutgoingCallHandler(CallEventListener callEventListener, CallParticipant cp) {

	addCallEventListener(this);
        csl = callEventListener;
        this.cp = cp;

	setName("Outgoing CallHandler for " + cp);
    }

    public CallEventListener getRequestHandler() {
        return csl;
    }

    /*
     * Thread to start a new call and join a conference.
     */
    private static int nCalls = 0;	// for debugging two gateways

    public void run() {
        /*
         * Join an existing conference or create a new one.
         */
        synchronized (ConferenceManager.getConferenceList()) {
            conferenceManager = ConferenceManager.getConference(cp);

            if (conferenceManager == null) {
                Logger.error("Couldn't start conference " + cp.getConferenceId());

                sendCallEventNotification( new CallEvent(CallEvent.CANT_START_CONFERENCE));
                return;
            }

            try {
                member = conferenceManager.joinConference(cp);
                memberSender = member.getMemberSender();
                memberReceiver = member.getMemberReceiver();
            } catch (IOException e) {
		CallEvent callEvent =
		    new CallEvent(CallEvent.CANT_CREATE_MEMBER);

		callEvent.setInfo(e.getMessage());

                sendCallEventNotification(callEvent);
        	removeCallEventListener(this);
                return;
            }
        }

	addCall(this);		// add to list of active calls

        lastGateway = false;
	onlyOneGateway = false;

        /*
         * Start the call (INVITE) and wait for it to end (BYE).
         */
        ArrayList voIPGateways = SipServer.getVoIPGateways();
        String gateway = cp.getVoIPGateway();

        if (gateway != null) {
            /*
             * User specified a specific gateway.  Use that one only.
             */
            Logger.println("Call " + this + ":  Using gateway specified for the call:  " + gateway);

            lastGateway = true;
	    onlyOneGateway = true;
            placeCall();
        } else if (voIPGateways.size() > 0) {

			if (voIPGateways.size() == 1) {
				onlyOneGateway = true;
			}

            lastGateway = true;
            placeCall();

        } else if (cp.getPhoneNumber() != null && cp.getPhoneNumber().indexOf("sip:") == 0) {

			 placeCall(); // no gateway involved, direct SIP call

		} else if (cp.getProtocol() != null && ("Speaker".equals(cp.getProtocol()) || "WebRtc".equals(cp.getProtocol()) || "Rtmfp".equals(cp.getProtocol()))) {

			 placeCall(); // WebRtc call

		} else {

			Logger.error("Couldn't place call " + cp);
			sendCallEventNotification( new CallEvent(CallEvent.CANT_START_CONFERENCE));
		}

        conferenceManager.leave(member); // Remove member from conference.

        removeCall(this);		 // remove call from active call list
	removeCallEventListener(this);
        done = true;
    }

    private void placeCall() {
	String protocol = Bridge.getDefaultProtocol();

	if (cp.getProtocol() != null) {
	    protocol = cp.getProtocol();
	}

	if (protocol.equalsIgnoreCase("SIP")) {
            csa = new SipTPCCallAgent(this);
	} else if (protocol.equalsIgnoreCase("NS")) {
	    csa = new NSOutgoingCallAgent(this);
	} else if (protocol.equalsIgnoreCase("WebRtc")) {
	    csa = new WebRtcCallAgent(this);
	} else if (protocol.equalsIgnoreCase("Speaker")) {
	    csa = new SpeakerCallAgent(this);
	} else if (protocol.equalsIgnoreCase("Rtmfp")) {
	    csa = new RtmfpCallAgent(this);
	} else {
	    //csa = new H323TPCCallAgent(this);
	    reasonCallEnded =
		CallEvent.getEventString(CallEvent.H323_NOT_IMPLEMENTED);

	    sendCallEventNotification(
		new CallEvent(CallEvent.H323_NOT_IMPLEMENTED));

	    Logger.println("Call " + cp + ":  " + reasonCallEnded);
	    return;
	}

        try {
            csa.initiateCall();

	    synchronized (callInitiatedLock) {
	        callInitiatedLock.notifyAll();
	    }

            synchronized(stateChangeLock) {
                if (reasonCallEnded == null) {

		    //if (protocol.equalsIgnoreCase("SIP") == false) {
		    //    /*
		    //     * Leave Conference and rejoin with the right local media parameters
		    //     * XXX Need to somehow get the socket from the h323 stack!
		    //     */
		    //    member.getMemberReceiver().setReceiveSocket();
		    //    conferenceManager.transferMember(conferenceManager, member);
		    //}

                    try {
                        stateChangeLock.wait();	// wait for call to end
                    } catch (InterruptedException e) {
                    }
                }
            }
        } catch (IOException e) {
	    synchronized (callInitiatedLock) {
	        callInitiatedLock.notifyAll();
	    }

	    if (reasonCallEnded == null) {
		cancelRequest(e.getMessage());
	    }

            Logger.println("Call " + this + " Exception " + e.getMessage());
        }
    }

    /*
     * This method is called where there is new status information.
     * Status can be a state change, dtmf key pressed,
     * or speaking not speaking notification.
     */
    public void callEventNotification(CallEvent callEvent) {
	if (Logger.logLevel >= Logger.LOG_INFO) {
	    Logger.println("Notification:  " + callEvent);
	}

	if (callEvent.equals(CallEvent.STATE_CHANGED)) {
	    if (callEvent.getCallState().equals(CallState.ANSWERED)) {

                /*
                 * For two party calls
                 */
                synchronized(waitCallAnswerLock) {
                    waitCallAnswerLock.notify();
                }
            } else if (callEvent.getCallState().equals(CallState.ESTABLISHED)) {
                /*
                 * For migrating calls
                 */
                synchronized(waitCallEstablishedLock) {
                    waitCallEstablishedLock.notify();
                }
            } else if (callEvent.getCallState().equals(CallState.ENDING)) {
                CallHandler callHandler =
                    CallHandler.findMigratingCall(cp.getCallId());

                if (callHandler == this) {
                    /*
                     * If it's a gateway error and it's not the last gateway,
                     * don't end the call.  It will be retried with the
                     * alternate gateway.
                     */
                    if (callEvent.getInfo().indexOf("gateway error") >= 0 &&
                            lastGateway == false) {

                        return;
                    }

		    callEvent = new CallEvent(CallEvent.MIGRATION_FAILED);

		    callEvent.setInfo("Migration failed: " + getReasonCallEnded());
                    sendCallEventNotification(callEvent);
                }
            } else if (callEvent.getCallState().equals(CallState.ENDED)) {
                reasonCallEnded = callEvent.getInfo();

                synchronized(waitCallAnswerLock) {
                    waitCallAnswerLock.notify();
                }

                if (reasonCallEnded.indexOf("gateway error") >= 0 &&
                        lastGateway == false) {

                    CallHandler callHandler =
                        CallHandler.findMigratingCall(cp.getCallId());

                    if (callHandler == this) {
                        synchronized(stateChangeLock) {
                            /*
                             * Let the outgoing call handler know so
                             * it can try another gateway.
                             */
                            stateChangeLock.notify();
                        }
                        return;	// don't tell the migrator yet
                    }
                }

                synchronized(waitCallEstablishedLock) {
                    waitCallEstablishedLock.notify();
                }

                synchronized(stateChangeLock) {
                    stateChangeLock.notify();	// the call has ended
                }

                /*
                 * If it's a gateway error and not the last gateway,
                 * don't end the call.  It will be retried with the
                 * alternate gateway.
                 */
                if (reasonCallEnded.indexOf("gateway error") >= 0 &&
                        lastGateway == false) {

                    return;
                }

                cancelRequest(reasonCallEnded);
            }
	}

        if (suppressEvent(cp, callEvent) == false) {
            Application.outgoingCallNotification(callEvent);

	    if (csl != null) csl.callEventNotification(callEvent);


        }
    }

    /*
     * This method is called by a CallSetupAgent once the endpoint
     * address is known.  The endpoint address is the address from which
     * we expect to receive RTP packets and to which we will send RTP packets.
     */
    //public void setEndpointAddress(InetSocketAddress isa, byte mediaPayload,
    //	    byte receivePayload, byte telephoneEventPayload) {
    //
    //    member.initialize(this, isa, mediaPayload, receivePayload, telephoneEventPayload);
    //}

    /*
     * To make call migration and automatic retries to alternate gateways
     * transparent to the facilitator, we need to suppress certain
     * status messages.
     */
    private boolean suppressEvent(CallParticipant cp, CallEvent callEvent) {
        /*
         * Suppress status from migrated calls so the facilitator
         * doesn't see CALL_ENDED from the previous call.
         *
         * XXX Not sure about this.  I think we want the status to go through.
         * The receiver of the status will see "migrated" in the message
         * and can decide what to do.
         *
         * For the new call, we allow "No Answer".  Once the call is answered
         * we clear the migrateCall flag so that CALL_ENDING and CALL_END
         * will be delivered to the client
         */
        if (suppressStatus == true) {
            if (callEvent.getInfo() != null &&
		    callEvent.getInfo().indexOf("No Answer") >= 0 ||
		    callEvent.equals(CallEvent.BUSY_HERE) ||
		    callEvent.equals(CallEvent.CALL_ANSWER_TIMEOUT) ||
		    callEvent.equals(CallEvent.MIGRATED) ||
		    callEvent.equals(CallEvent.MIGRATION_FAILED) ||
		    callEvent.equals(CallEvent.JOIN_TIMEOUT)) {

                return false;
            }

            return true;
        }

        /*
         * We automatically retry calls with an alternate gateway
         * when there is a gateway error.  The status sent to the
         * socket should make the switch to the alternate gateway transparent.
         * We don't want to send CALL_ENDING or CALL_ENDED until
         * we've tried the alternate gateway.
         * We also want to suppress CALL_PARTICIPANT_INVITED when
         * trying the alternate gateway.
         */
        if (lastGateway == false) {
            /*
             * Suppress gateway errors from default gateway
             */
            if (callEvent.getInfo().indexOf("gateway error") >= 0) {
                return true;
            }

            return false;
        }

        /*
         * Suppress CALL_PARTICIPANT_INVITED message from alternate gateway
         */
	if (onlyOneGateway == false && callEvent.equals(CallEvent.STATE_CHANGED) &&
	        callEvent.getCallState().equals(CallState.INVITED)) {

            return true;
        }

        /*
         * No need to suppress this message.
         */
        return false;
    }

    /*
     * terminate a call.
     */
    public void cancelRequest(String reason) {
        done = true;

        if (csa != null) {
            CallHandler migratingCall =
                    CallHandler.findMigratingCall(cp.getCallId());

            if (migratingCall == this) {
                Logger.println("Failed to Migrate:  " + reason);
            }

            csa.cancelRequest(reason);
        }

        synchronized(waitCallAnswerLock) {
            waitCallAnswerLock.notify();
        }

        CallHandler otherCall = this.otherCall;

        this.otherCall = null;

        if (otherCall != null) {
            Logger.println("otherCall is " + otherCall.getCallParticipant());

            otherCall.cancelRequest("Two party call ended");
        }
    }

    public String getSdp() {
	synchronized (callInitiatedLock) {
            while (csa == null && !done && reasonCallEnded == null) {
		try {
		    callInitiatedLock.wait();
		} catch (InterruptedException e) {
		}
	    }
	    return csa.getSdp();
	}
    }

    /*
     * For two party calls.
     *
     * When one party hangs up, the other call should be terminated as well.
     */
    //private OutgoingCallHandler otherCall;

    public void setOtherCall(OutgoingCallHandler otherCall) {
        this.otherCall = otherCall;
    }

    /*
     * For two party calls, we wait until the first party answers
     * before calling the second party.
     *
     * When the first party answers, the second party is called and
     * the treatment is played to the first party.
     *
     * When the second party answers, the treatment to the first party
     * is stopped.
     */
    public boolean waitForCallToBeAnswered() {

		String protocol = Bridge.getDefaultProtocol();

		if (cp.getProtocol() != null) {
			protocol = cp.getProtocol();
		}

		if (protocol.equalsIgnoreCase("WebRtc") || protocol.equalsIgnoreCase("Rtmfp")  || protocol.equalsIgnoreCase("Speaker")) {
			return true;
		}

        synchronized(waitCallAnswerLock) {
            if (done || reasonCallEnded != null) {
                return false;
            }

            try {
                waitCallAnswerLock.wait();
            } catch (InterruptedException e) {
            }
        }

        if (done || reasonCallEnded != null) {
            return false;
        }

        return true;
    }

    public boolean waitForCallToBeEstablished() {

		if (cp.getProtocol().equalsIgnoreCase("WebRtc") || cp.getProtocol().equalsIgnoreCase("Rtmfp") || cp.getProtocol().equalsIgnoreCase("Speaker")) {
			return true;
		}

        synchronized(waitCallEstablishedLock) {
            if (done || reasonCallEnded != null) {
                return false;
            }

            try {
                waitCallEstablishedLock.wait();
            } catch (InterruptedException e) {
            }
        }

        if (done || reasonCallEnded != null) {
            return false;
        }

        return true;
    }

    /*
     * Cancel all calls started by the specified requestHandler
     */
    public static void hangup(CallEventListener callEventListener, String reason) {

        ArrayList<CallHandler> callsToCancel = new ArrayList();

        synchronized(activeCalls) {
            /*
             * Make a list of all the calls we want to cancel, then cancel them.
             * We have to cancel them while not synchronized or
             * we could deadlock.
             */
            for (int i = 0; i < activeCalls.size(); i++) {
                CallHandler call = (CallHandler)activeCalls.elementAt(i);

                if (call.getRequestHandler() == callEventListener) {
                    callsToCancel.add(call);
                }
            }
        }

        cancel(callsToCancel, reason);
    }

    private static void cancel(ArrayList<CallHandler> callsToCancel, String reason) {
        while (callsToCancel.size() > 0) {
            CallHandler call = callsToCancel.remove(0);
            call.cancelRequest(reason);
        }
    }

    /**
     * String representation of this OutgoingCallHandler
     * @return the string representation of this OutgoingCallHandler
     */
    public String toString() {
        return cp.toString();
    }

}
