/*
 * 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.Logger;
import com.sun.voip.MediaInfo;
import com.sun.voip.TreatmentDoneListener;
import com.sun.voip.TreatmentManager;

import java.io.IOException;

import java.net.InetSocketAddress;

import java.text.ParseException;

import java.util.NoSuchElementException;

/**
 * Super class with code common to both Sip User Agents and 
 * non-SIP Agents.
 * 
 * This class is responsible for playing treatments, getting notification
 * when done, managing the state of active calls.
 */
public class CallSetupAgent implements TreatmentDoneListener, Runnable {

    protected TreatmentManager callAnsweredTreatment = null;

    private TreatmentManager callEstablishedTreatment = null;

    protected String reasonCallTerminated;

    protected CallState callState = new CallState();

    protected CallHandler callHandler;
    protected CallParticipant cp;

    private Thread inviteTimeoutThread;
    private static int defaultCallAnswerTimeout = 90;  // 90 seconds

    /**
     * Constructor
     * @param callHandler CallHandler this CallParticipant is associated with
     * @param cp the CallParticipant
     */
    public CallSetupAgent(CallHandler callHandler) {
	this.callHandler = callHandler;

	cp = callHandler.getCallParticipant();
    }
    
    /*
     * INVITE timeout thread, handle call not answered
     */
    public void run() {
        int timeout = cp.getCallAnswerTimeout();

        if (timeout == 0) {
            timeout = defaultCallAnswerTimeout;
        }

        try {
            Thread.sleep(timeout * 1000);
        } catch (InterruptedException e) {
        }

	inviteTimeoutThread = null;

        if (reasonCallTerminated == null && getState() < CallState.ANSWERED) {
	    Logger.println("Call answer time out " + cp);
	    sendCallEventNotification(
		new CallEvent(CallEvent.CALL_ANSWER_TIMEOUT));
            cancelRequest("No answer");
        }
    }

    /**
     * set the call answer timeout in seconds
     */
    public static void setDefaultCallAnswerTimeout(
	    int defaultCallAnswerTimeout) {

        CallSetupAgent.defaultCallAnswerTimeout = defaultCallAnswerTimeout;
    }

    /**
     * Get the call answer timeout in seconds
     */
    public static int getDefaultCallAnswerTimeout() {
        return defaultCallAnswerTimeout;
    }

    public CallState getCallState() {
	return callState;
    }

    public int getState() {
	return callState.getState();
    }

    protected void setState(int state) {
	setState(state, null);
    }

    protected void setState(int state, String info) {
	callState = new CallState(state);

	CallEvent callEvent = new CallEvent(CallEvent.STATE_CHANGED);

	callEvent.setCallState(callState);

	String s = "";

	if (state == CallState.INVITED || state == CallState.ESTABLISHED) {
	    s += "ConferenceReceiverPort='" 
		+ callHandler.getReceiveAddress().getPort() + "'";

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

            s += " ConferencePayload='" +  mediaInfo.getPayload() + "'";
	    s += " BridgeIPAddress='" 
		+ Bridge.getPrivateHost() + "'";

	    s += " BridgeInfo='" 
                + Bridge.getPrivateHost() + ":"
                + Bridge.getPrivateControlPort() + ":"
                + Bridge.getPrivateSipPort() + ":" 
		+ Bridge.getPublicHost() + ":"
                + Bridge.getPublicControlPort()+ ":"
                + Bridge.getPublicSipPort() + "'";
	} 

	if (info != null) {
	    s = info + " " + s;
	}

	callEvent.setInfo(s);

	Logger.println("Call " + callHandler + " " + callState);

	sendCallEventNotification(callEvent);

        if (state == CallState.ESTABLISHED) {
            String treatment = cp.getCallEstablishedTreatment();

            if (treatment != null) {
		callEstablishedTreatment = initializeTreatment(treatment, 0);

		if (callEstablishedTreatment != null) {
		    addTreatment(callEstablishedTreatment);
		}
            }
	}

	if (inviteTimeoutThread == null && state == CallState.INVITED) {
	    inviteTimeoutThread = new Thread(this);
            inviteTimeoutThread.start();        // start invite timeout thread
	}
    }

    protected void sendCallEventNotification(CallEvent callEvent) {
	callHandler.sendCallEventNotification(callEvent);
    }

    /*
     * Send the address of the endpoint to the CallHandler so 
     * the CallHandler can tell the Mixer where to send RTP data.
     */
    protected void setEndpointAddress(InetSocketAddress isa, 
	    byte mediaPayload, byte receivePayload,
	    byte telephoneEventPayload) {

	setEndpointAddress(isa, mediaPayload, receivePayload, telephoneEventPayload, null);
    }

    protected void setEndpointAddress(InetSocketAddress isa, 
	    byte mediaPayload, byte receivePayload,
	    byte telephoneEventPayload, InetSocketAddress rtcpAddress) {

	callHandler.setEndpointAddress(isa, mediaPayload, receivePayload,
	    telephoneEventPayload, rtcpAddress);
    }

    /*
     * true if call is established
     */
    public boolean isCallEstablished() {
	return getState() == CallState.ESTABLISHED || 
	    getState() == CallState.ENDING;
    }

    public boolean isCallEnding() {
	return getState() == CallState.ENDING || 
	    getState() == CallState.ENDED;
    }

    /**
     * Setup the call answerered treatment manager
     */
    protected void initializeCallAnsweredTreatment() {
        String treatment = cp.getCallAnsweredTreatment();

        int repeatCount = 0;

        if (cp.getJoinConfirmationTimeout() != 0) {
            /*
             * If join confirmation is requested, we will repeat
             * the message.  This is needed because AccessLine
             * answers the call before a person actually gets connected
             * to hear the message.
             */
            repeatCount = 30;
        }

	callAnsweredTreatment = initializeTreatment(treatment, repeatCount);

        if (callHandler.isFirstMember()) {
            treatment = cp.getFirstConferenceMemberTreatment();

            if (treatment != null) {
                callAnsweredTreatment =
		    initializeTreatment(treatment, repeatCount);
            }
        }
    }

    private TreatmentManager initializeTreatment(String treatment,
	    int repeatCount) {

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

        if (treatment != null) {
	    try {
                return new TreatmentManager(
                    treatment, repeatCount, mediaInfo.getSampleRate(),
		    mediaInfo.getChannels());
	    } catch (IOException e) {
		Logger.println("can't play treatment " + treatment
		    + " " + e.getMessage());
	    }
        }
	return null;
    }

    /*
     * Add a treatment to play to the call.
     */
    protected void addTreatment(TreatmentManager treatmentManager) {
	callHandler.addTreatment(treatmentManager);
    }

    /**
     * Start call answered treatment
     */
    protected void startCallAnsweredTreatment() {
        Logger.println("Call " + callHandler 
	    + " starting call answered treatment");

        callAnsweredTreatment.addTreatmentDoneListener(this);
        callHandler.addTreatment(callAnsweredTreatment);
    }

    /**
     * Stop call answered treatment
     */
    public void stopCallAnsweredTreatment() {
	if (callAnsweredTreatment != null) {
            if (Logger.logLevel >= Logger.LOG_MOREINFO) {
	        Logger.println("Call " + callHandler 
		    + " Stop callAnsweredTreatment player...");
	    }
	
	    callAnsweredTreatment.stopTreatment();
	    callAnsweredTreatment = null;
	} else {
	    if (cp.getJoinConfirmationTimeout() != 0) {
		if (getState() == CallState.INVITED) {
		    setState(CallState.ANSWERED);
		
		    MediaInfo mediaInfo = 
			callHandler.getConferenceManager().getMediaInfo();

		    setState(CallState.ESTABLISHED,
			"ConferencePayload='" +  mediaInfo.getPayload() + "'"
	    		+ " BridgeIPAddress='" 
			+ Bridge.getPrivateHost() + "'");
		}
	    }
	}
    }

    public void stopCallEstablishedTreatment() {
        if (callEstablishedTreatment == null) {
	    return;
	}

        if (Logger.logLevel >= Logger.LOG_MOREINFO) {
            Logger.println("Call " + callHandler
                + " Stop callEstablishedTreatment player...");
        }

        callEstablishedTreatment.stopTreatment();
        callEstablishedTreatment = null;
    }

    /**
     * Notification that a treatment has finished
     *
     * The state we are in determines what we do next.
     */
    public void treatmentDoneNotification(TreatmentManager treatmentManager) {
        if (Logger.logLevel >= Logger.LOG_MOREINFO) {
            Logger.println("Call " + callHandler 
		+ " treatment done notification, current state "
                + callState + " " + callHandler);
        }

        switch (getState()) {
        case CallState.INVITED:
            /*
             * When join confirmation is requested, we stay in the
             * CALL_PARTICIPANT_INVITED state until a dtmf key is pressed.
             * Then we change state to CALL_PARTICIPANT_ANSWERED and
             * fall through.
	     *
	     * XXX We need to make sure the treatment repeats enough times.
	     * If it finishes before we timeout, we'll treat the call as answered!
	     *
             */
	    if (reasonCallTerminated != null) {
		break;
	    }

            setState(CallState.ANSWERED);
	    
        case CallState.ANSWERED:
	    /*
	     * Call answered treatment is done, we're ready for the conference
	     */
	    MediaInfo mediaInfo = 
		callHandler.getConferenceManager().getMediaInfo();

	    setState(CallState.ESTABLISHED,
		"ConferencePayload='" +  mediaInfo.getPayload() + "'"
		+ " BridgeIPAddress='" 
		+ Bridge.getPrivateHost() + "'");
            break;

	case CallState.ENDING:
	    /*
	     * Call end treatment is done, time to end the call
	     */
	    terminateCall();
	    done();
	    break;

	default:
	    Logger.error("Call " + callHandler 
		+ ":  unexpected state " + callState);
            break;
        }
    }

    /**
     * A CallSetupAgent may override the following methods:
     *
     * initiateCall() to start a call
     * terminateCall() class specific code to end a call.
     */
    public void initiateCall() throws IOException {
    }

    public String getSdp() {
	return null;
    }

    public void setRemoteMediaInfo(String sdp) throws ParseException {
    }

    public void terminateCall() {
    }

    /**
     * Cancel a call
     */
    public void cancelRequest(String s) {
	if (reasonCallTerminated != null || getState() == CallState.ENDED) {
	    return;
	}

	reasonCallTerminated = s;

        if (inviteTimeoutThread != null) {
            inviteTimeoutThread.interrupt();
            inviteTimeoutThread = null;
        }

	if (Logger.logLevel >= Logger.LOG_INFO) {
            Logger.println(
		"Call " + callHandler + ":  cancelling call, " + s);
	}

	if (callAnsweredTreatment != null) {
	    callAnsweredTreatment.stopTreatment();
	}

	if (callEstablishedTreatment != null) {
	    callEstablishedTreatment.stopTreatment();
	}

	if (getState() == CallState.ESTABLISHED) {
	    String endTreatment = cp.getCallEndTreatment();

	    if (endTreatment != null && cp.isConferenceMuted() == false) {
		try {
        	    MediaInfo mediaInfo = 
			callHandler.getConferenceManager().getMediaInfo();

    		    TreatmentManager callEndTreatment =
			new TreatmentManager(endTreatment, 0, 
			    mediaInfo.getSampleRate(), mediaInfo.getChannels());

		    setState(CallState.ENDING, "Reason='" + s + "'");

		    if (Logger.logLevel >= Logger.LOG_MOREINFO) {
	                Logger.println("Call " + callHandler 
			    + " adding end treatment...");
		    }

		    callEndTreatment.addTreatmentDoneListener(this);
                    callHandler.addTreatment(callEndTreatment);

		    return;
		} catch (IOException e) {
		    Logger.error("Call " + callHandler + " " + e.getMessage());
		}
	    }
	} 

	terminateCall();	// do subclass specific work
	setState(CallState.ENDING, "Reason='" + s + "'");
	done();
    }

    /*
     * Finish the call termination process
     */
    private void done() {
	if (getState() == CallState.ENDED) {
	    return;
	}

	setState(CallState.ENDED, "Reason='" + reasonCallTerminated + "'");
    }

}
