/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jitsi.impl.neomedia;

import java.beans.*;
import java.util.*;

import javax.media.*;
import javax.media.control.*;
import javax.media.format.*;

import org.jitsi.impl.neomedia.device.*;
import org.jitsi.impl.neomedia.transform.dtmf.*;
import org.jitsi.service.configuration.*;
import org.jitsi.service.libjitsi.*;
import org.jitsi.service.neomedia.*;
import org.jitsi.service.neomedia.codec.*;
import org.jitsi.service.neomedia.device.*;
import org.jitsi.service.neomedia.event.*;
import org.jitsi.service.protocol.*;
import org.jitsi.util.*;
import org.jitsi.util.event.*;

/**
 * Extends <tt>MediaStreamImpl</tt> in order to provide an implementation of
 * <tt>AudioMediaStream</tt>.
 *
 * @author Lyubomir Marinov
 * @author Emil Ivov
 */
public class AudioMediaStreamImpl
    extends MediaStreamImpl
    implements AudioMediaStream,
               PropertyChangeListener
{

    /**
     * List of RTP format strings which are supported by SIP Communicator in
     * addition to the JMF standard formats.
     *
     * @see #registerCustomCodecFormats(StreamRTPManager)
     */
    private static final AudioFormat[] CUSTOM_CODEC_FORMATS
        = new AudioFormat[]
                {
                    /*
                     * these formats are specific, since RTP uses format numbers
                     * with no parameters.
                     */
                    new AudioFormat(
                            Constants.ALAW_RTP,
                            8000,
                            8,
                            1,
                            Format.NOT_SPECIFIED,
                            AudioFormat.SIGNED),
                    new AudioFormat(
                            Constants.G722_RTP,
                            8000,
                            Format.NOT_SPECIFIED /* sampleSizeInBits */,
                            1)
                };

    /**
     * The <tt>Logger</tt> used by the <tt>AudioMediaStreamImpl</tt> class and
     * its instances for logging output.
     */
    private static final Logger logger
        = Logger.getLogger(AudioMediaStreamImpl.class);

    /**
     * A <tt>PropertyChangeNotifier<tt> which will inform this
     * <tt>AudioStream</tt> if a selected audio device (capture, playback or
     * notification device) has changed. We want to listen to these events,
     * especially for those generated after the <tt>AudioSystem</tt> has
     * changed.
     */
    private final PropertyChangeNotifier audioSystemChangeNotifier;

    /**
     * The listener that gets notified of changes in the audio level of
     * remote conference participants.
     */
    private CsrcAudioLevelListener csrcAudioLevelListener;

    /**
     * The list of DTMF listeners.
     */
    private final List<DTMFListener> dtmfListeners
        = new ArrayList<DTMFListener>();

    /**
     * The transformer that we use for sending and receiving DTMF packets.
     */
    private DtmfTransformEngine dtmfTransfrmEngine;

    /**
     * The listener which has been set on this instance to get notified of
     * changes in the levels of the audio that the local peer/user is sending to
     * the remote peer(s).
     */
    private SimpleAudioLevelListener localUserAudioLevelListener;

    /**
     * The listener which has been set on this instance to get notified of
     * changes in the levels of the audios that the local peer/user is receiving
     * from the remote peer(s).
     */
    private SimpleAudioLevelListener streamAudioLevelListener;

    /**
     * Initializes a new <tt>AudioMediaStreamImpl</tt> instance which will use
     * the specified <tt>MediaDevice</tt> for both capture and playback of audio
     * exchanged via the specified <tt>StreamConnector</tt>.
     *
     * @param connector the <tt>StreamConnector</tt> the new instance is to use
     * for sending and receiving audio
     * @param device the <tt>MediaDevice</tt> the new instance is to use for
     * both capture and playback of audio exchanged via the specified
     * <tt>StreamConnector</tt>
     * @param srtpControl a control which is already created, used to control
     * the srtp operations.
     */
    public AudioMediaStreamImpl(
            StreamConnector connector,
            MediaDevice device,
            SrtpControl srtpControl)
    {
        super(connector, device, srtpControl);

        MediaService mediaService = LibJitsi.getMediaService();

        if (mediaService instanceof PropertyChangeNotifier)
        {
            audioSystemChangeNotifier = (PropertyChangeNotifier) mediaService;
            audioSystemChangeNotifier.addPropertyChangeListener(this);
        }
        else
            audioSystemChangeNotifier = null;
    }

    /**
     * Adds a <tt>DTMFListener</tt> to this <tt>AudioMediaStream</tt> which is
     * to receive notifications when the remote party starts sending DTMF tones
     * to us.
     *
     * @param listener the <tt>DTMFListener</tt> to register for notifications
     * about the remote party starting sending of DTM tones to this
     * <tt>AudioMediaStream</tt>
     * @see AudioMediaStream#addDTMFListener(DTMFListener)
     */
    public void addDTMFListener(DTMFListener listener)
    {
        if((listener != null) && !dtmfListeners.contains(listener))
            dtmfListeners.add(listener);
    }

    /**
     * In addition to calling
     * {@link MediaStreamImpl#addRTPExtension(byte, RTPExtension)}
     * this method enables sending of CSRC audio levels. The reason we are
     * doing this here rather than in the super class is that CSRC levels only
     * make sense for audio streams so we don't want them enabled in any other
     * type.
     *
     * @param extensionID the ID assigned to <tt>rtpExtension</tt> for the
     * lifetime of this stream.
     * @param rtpExtension the RTPExtension that is being added to this stream.
     */
    @Override
    public void addRTPExtension(byte extensionID, RTPExtension rtpExtension)
    {
        super.addRTPExtension(extensionID, rtpExtension);

        if (RTPExtension.CSRC_AUDIO_LEVEL_URN.equals(
                rtpExtension.getURI().toString()))
        {
            getCsrcEngine().setCsrcAudioLevelAudioLevelExtensionID(
                    extensionID,
                    rtpExtension.getDirection());
        }
    }

    /**
     * Delivers the <tt>audioLevels</tt> map to whoever's interested. This
     * method is meant for use primarily by the transform engine handling
     * incoming RTP packets (currently <tt>CsrcTransformEngine</tt>).
     *
     * @param audioLevels a array mapping CSRC IDs to audio levels in
     * consecutive elements.
     */
    public void audioLevelsReceived(long[] audioLevels)
    {
        CsrcAudioLevelListener csrcAudioLevelListener
            = this.csrcAudioLevelListener;

        if (csrcAudioLevelListener != null)
            csrcAudioLevelListener.audioLevelsReceived(audioLevels);
    }

    /**
     * Releases the resources allocated by this instance in the course of its
     * execution and prepares it to be garbage collected.
     *
     * @see MediaStream#close()
     */
    @Override
    public void close()
    {
        super.close();

        if(dtmfTransfrmEngine != null)
        {
            dtmfTransfrmEngine.close();
            dtmfTransfrmEngine = null;
        }

        if (audioSystemChangeNotifier != null)
            audioSystemChangeNotifier.removePropertyChangeListener(this);
    }

    /**
     * Performs any optional configuration on the <tt>BufferControl</tt> of the
     * specified <tt>RTPManager</tt> which is to be used as the
     * <tt>RTPManager</tt> of this <tt>MediaStreamImpl</tt>.
     *
     * @param rtpManager the <tt>RTPManager</tt> which is to be used by this
     * <tt>MediaStreamImpl</tt>
     * @param bufferControl the <tt>BufferControl</tt> of <tt>rtpManager</tt> on
     * which any optional configuration is to be performed
     */
    @Override
    protected void configureRTPManagerBufferControl(
            StreamRTPManager rtpManager,
            BufferControl bufferControl)
    {
        /*
         * It appears that, if we don't do the following, the RTPManager won't
         * play.
         */
        ConfigurationService cfg = LibJitsi.getConfigurationService();
        /*
         * There isn't a particular reason why we'd choose 100 or 120. It may be
         * that 120 is divided by 30 (which is used by iLBC, for example) and
         * 100 isn't. Anyway, what matters most is that it's proportional to the
         * latency of the playback. 
         */
        long bufferLength = 120;

        if (cfg != null)
        {
            String bufferLengthStr
                = cfg.getString(PROPERTY_NAME_RECEIVE_BUFFER_LENGTH);

            try
            {
                if ((bufferLengthStr != null) && (bufferLengthStr.length() > 0))
                    bufferLength = Long.parseLong(bufferLengthStr);
            }
            catch (NumberFormatException nfe)
            {
                logger.warn(
                        bufferLengthStr
                            + " is not a valid receive buffer length/long value",
                        nfe);
            }
        }

        bufferLength = bufferControl.setBufferLength(bufferLength);
        if (logger.isTraceEnabled())
            logger.trace("Set receiver buffer length to " + bufferLength);

        /*
         * The threshold should better be half of the bufferLength rather than
         * equal to it (as it used to be before). Whatever it is, FMJ/JMF
         * doesn't take it into account anyway.
         */
        long minimumThreshold = bufferLength / 2;

        bufferControl.setEnabledThreshold(minimumThreshold > 0);
        bufferControl.setMinimumThreshold(minimumThreshold);
    }

    /**
     * A stub that allows audio oriented streams to create and keep a reference
     * to a <tt>DtmfTransformEngine</tt>.
     *
     * @return a <tt>DtmfTransformEngine</tt> if this is an audio oriented
     * stream and <tt>null</tt> otherwise.
     */
    @Override
    protected DtmfTransformEngine createDtmfTransformEngine()
    {
        if(this.dtmfTransfrmEngine == null)
            this.dtmfTransfrmEngine = new DtmfTransformEngine(this);
        return this.dtmfTransfrmEngine;
    }

    /**
     * {@inheritDoc}
     *
     * Makes sure that {@link #localUserAudioLevelListener} and
     * {@link #streamAudioLevelListener} which have been set on this
     * <tt>AudioMediaStream</tt> will be automatically updated when a new
     * <tt>MediaDevice</tt> is set on this instance.
     */
    @Override
    protected void deviceSessionChanged(
            MediaDeviceSession oldValue,
            MediaDeviceSession newValue)
    {
        try
        {
            if (oldValue != null)
            {
                AudioMediaDeviceSession deviceSession
                    = (AudioMediaDeviceSession) oldValue;

                if (localUserAudioLevelListener != null)
                    deviceSession.setLocalUserAudioLevelListener(null);
                if (streamAudioLevelListener != null)
                    deviceSession.setStreamAudioLevelListener(null);
            }
            if (newValue != null)
            {
                AudioMediaDeviceSession deviceSession
                    = (AudioMediaDeviceSession) newValue;

                if (localUserAudioLevelListener != null)
                {
                    deviceSession.setLocalUserAudioLevelListener(
                            localUserAudioLevelListener);
                }
                if (streamAudioLevelListener != null)
                {
                    deviceSession.setStreamAudioLevelListener(
                            streamAudioLevelListener);
                }
            }
        }
        finally
        {
            super.deviceSessionChanged(oldValue, newValue);
        }
    }

    /**
     * Delivers the <tt>DTMF</tt> tones. The method is meant for use primarily
     * by the transform engine handling incoming RTP packets (currently
     * <tt>DtmfTransformEngine</tt>).
     *
     * @param tone the new tone
     * @param end <tt>true</tt> if the tone is to be ended or <tt>false</tt> to
     * be started
     */
    public void fireDTMFEvent(DTMFRtpTone tone, boolean end)
    {
        DTMFToneEvent ev = new DTMFToneEvent(this, tone);

        for (DTMFListener listener : dtmfListeners)
        {
            if(end)
                listener.dtmfToneReceptionEnded(ev);
            else
                listener.dtmfToneReceptionStarted(ev);
        }
    }

    /**
     * Returns the <tt>MediaDeviceSession</tt> associated with this stream
     * after first casting it to <tt>AudioMediaDeviceSession</tt> since this is,
     * after all, an <tt>AudioMediaStreamImpl</tt>.
     *
     * @return the <tt>AudioMediaDeviceSession</tt> associated with this stream.
     */
    @Override
    public AudioMediaDeviceSession getDeviceSession()
    {
        return (AudioMediaDeviceSession) super.getDeviceSession();
    }

    /**
     * Returns the last audio level that was measured by the underlying device
     * session for the specified <tt>ssrc</tt> (where <tt>ssrc</tt> could also
     * correspond to our local sync source identifier).
     *
     * @param ssrc the SSRC ID whose last measured audio level we'd like to
     * retrieve.
     *
     * @return the audio level that was last measured for the specified
     * <tt>ssrc</tt> or <tt>-1</tt> if no level has been cached for that ID.
     */
    public int getLastMeasuredAudioLevel(long ssrc)
    {
        AudioMediaDeviceSession devSession = getDeviceSession();

        if (devSession == null)
            return -1;
        else if (ssrc == getLocalSourceID())
            return devSession.getLastMeasuredLocalUserAudioLevel();
        else
            return devSession.getLastMeasuredAudioLevel(ssrc);
    }

    /**
     * The priority of the audio is 3, which is meant to be higher than
     * other threads and higher than the video one.
     * @return audio priority.
     */
    @Override
    protected int getPriority()
    {
        return 3;
    }

    /**
     * Receives and reacts to property change events: if the selected device
     * (for capture, playback or notifications) has changed, then create or
     * recreate the streams in order to use it. We want to listen to these
     * events, especially for those generated after the audio system has
     * changed.
     *
     * @param ev The event which may contain a audio system change event.
     */
    public void propertyChange(PropertyChangeEvent ev)
    {
        /*
         * FIXME It is very wrong to do the following upon every
         * PropertyChangeEvent fired by MediaServiceImpl. Moreover, it does not
         * seem right that we'd want to start this MediaStream upon a
         * PropertyChangeEvent (regardless of its specifics).
         */
        if (sendStreamsAreCreated)
            recreateSendStreams();
        else
            start();
    }

    /**
     * Registers {@link #CUSTOM_CODEC_FORMATS} with a specific
     * <tt>RTPManager</tt>.
     *
     * @param rtpManager the <tt>RTPManager</tt> to register
     * {@link #CUSTOM_CODEC_FORMATS} with
     * @see MediaStreamImpl#registerCustomCodecFormats(StreamRTPManager)
     */
    @Override
    protected void registerCustomCodecFormats(StreamRTPManager rtpManager)
    {
        super.registerCustomCodecFormats(rtpManager);

        for (AudioFormat format : CUSTOM_CODEC_FORMATS)
        {
            if (logger.isDebugEnabled())
                logger.debug("registering format " + format +
                        " with RTP manager");

            /*
             * NOTE (mkoch@rowa.de): com.sun.media.rtp.RtpSessionMgr.addFormat
             * leaks memory, since it stores the Format in a static Vector.
             * AFAIK there is no easy way around it, but the memory impact
             * should not be too bad.
             */
            rtpManager.addFormat( format,
                        MediaUtils.getRTPPayloadType(
                            format.getEncoding(), format.getSampleRate()));
        }
    }

    /**
     * Removes <tt>listener</tt> from the list of <tt>DTMFListener</tt>s
     * registered with this <tt>AudioMediaStream</tt> to receive notifications
     * about incoming DTMF tones.
     *
     * @param listener the <tt>DTMFListener</tt> to no longer be notified by
     * this <tt>AudioMediaStream</tt> about incoming DTMF tones
     * @see AudioMediaStream#removeDTMFListener(DTMFListener)
     */
    public void removeDTMFListener(DTMFListener listener)
    {
        dtmfListeners.remove(listener);
    }

    /**
     * Registers <tt>listener</tt> as the <tt>CsrcAudioLevelListener</tt> that
     * will receive notifications for changes in the levels of conference
     * participants that the remote party could be mixing.
     *
     * @param listener the <tt>CsrcAudioLevelListener</tt> that we'd like to
     * register or <tt>null</tt> if we'd like to stop receiving notifications.
     */
    public void setCsrcAudioLevelListener(CsrcAudioLevelListener listener)
    {
        csrcAudioLevelListener = listener;
    }

    /**
     * Sets <tt>listener</tt> as the <tt>SimpleAudioLevelListener</tt>
     * registered to receive notifications from our device session for changes
     * in the levels of the audio that this stream is sending out.
     *
     * @param listener the <tt>SimpleAudioLevelListener</tt> that we'd like to
     * register or <tt>null</tt> if we want to stop local audio level
     * measurements.
     */
    public void setLocalUserAudioLevelListener(
            SimpleAudioLevelListener listener)
    {
        if (localUserAudioLevelListener != listener)
        {
            localUserAudioLevelListener = listener;

            AudioMediaDeviceSession deviceSession = getDeviceSession();

            if (deviceSession != null)
            {
                deviceSession.setLocalUserAudioLevelListener(
                        localUserAudioLevelListener);
            }
        }
    }

    /**
     * Sets <tt>listener</tt> as the <tt>SimpleAudioLevelListener</tt>
     * registered to receive notifications from our device session for changes
     * in the levels of the party that's at the other end of this stream.
     *
     * @param listener the <tt>SimpleAudioLevelListener</tt> that we'd like to
     * register or <tt>null</tt> if we want to stop stream audio level
     * measurements.
     */
    public void setStreamAudioLevelListener(SimpleAudioLevelListener listener)
    {
        if (streamAudioLevelListener != listener)
        {
            streamAudioLevelListener = listener;

            AudioMediaDeviceSession deviceSession = getDeviceSession();

            if (deviceSession != null)
            {
                deviceSession.setStreamAudioLevelListener(
                        streamAudioLevelListener);
            }
        }
    }

    /**
     * Starts sending the specified <tt>DTMFTone</tt> until the
     * <tt>stopSendingDTMF()</tt> method is called (Excepts for INBAND DTMF,
     * which stops by itself this is why where there is no need to call the
     * stopSendingDTMF). Callers should keep in mind the fact that calling this
     * method would most likely interrupt all audio transmission until the
     * corresponding stop method is called. Also, calling this method
     * successively without invoking the corresponding stop method between the
     * calls will simply replace the <tt>DTMFTone</tt> from the first call with
     * that from the second.
     *
     * @param tone the <tt>DTMFTone</tt> to start sending.
     * @param dtmfMethod The kind of DTMF used (RTP, SIP-INOF or INBAND).
     * @param minimalToneDuration The minimal DTMF tone duration.
     *
     * @throws IllegalArgumentException if <tt>dtmfMethod</tt> is not one of
     * {@link DTMFMethod#INBAND_DTMF}, {@link DTMFMethod#RTP_DTMF}, and
     * {@link DTMFMethod#SIP_INFO_DTMF}
     * @see AudioMediaStream#startSendingDTMF(DTMFTone, DTMFMethod)
     */
    public void startSendingDTMF(
            DTMFTone tone,
            DTMFMethod dtmfMethod,
            int minimalToneDuration)
    {
        switch (dtmfMethod)
        {
        case INBAND_DTMF:
            MediaDeviceSession deviceSession = getDeviceSession();

            if (deviceSession != null)
                deviceSession.addDTMF(DTMFInbandTone.mapTone(tone));
            break;

        case RTP_DTMF:
            if (dtmfTransfrmEngine != null)
            {
                DTMFRtpTone t = DTMFRtpTone.mapTone(tone);

                if (t != null)
                    dtmfTransfrmEngine.startSending(t, minimalToneDuration);
            }
            break;

        case SIP_INFO_DTMF:
            // This kind of DTMF is not managed directly by the
            // OperationSetDTMFSipImpl.
            break;

        default:
            throw new IllegalArgumentException("dtmfMethod");
        }
    }

    /**
     * Interrupts transmission of a <tt>DTMFTone</tt> started with the
     * <tt>startSendingDTMF()</tt> method. Has no effect if no tone is currently
     * being sent.
     *
     * @param dtmfMethod The kind of DTMF used (RTP, SIP-INOF or INBAND).
     * @throws IllegalArgumentException if <tt>dtmfMethod</tt> is not one of
     * {@link DTMFMethod#INBAND_DTMF}, {@link DTMFMethod#RTP_DTMF}, and
     * {@link DTMFMethod#SIP_INFO_DTMF}
     * @see AudioMediaStream#stopSendingDTMF(DTMFMethod)
     */
    public void stopSendingDTMF(DTMFMethod dtmfMethod)
    {
        switch (dtmfMethod)
        {
        case INBAND_DTMF:
            // The INBAND DTMF is sent by impluse of constant duration and does
            // not need to be stopped explicitly.
            break;

        case RTP_DTMF:
            if(dtmfTransfrmEngine != null)
                dtmfTransfrmEngine.stopSendingDTMF();
            break;

        case SIP_INFO_DTMF:
            // The SIP-INFO DTMF is managed directly by the
            // OperationSetDTMFSipImpl.
            break;

        default:
            throw new IllegalArgumentException("dtmfMethod");
        }
    }
}