/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * 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.sip.communicator.service.protocol;

import java.util.*;

import net.java.sip.communicator.service.protocol.event.*;

import org.jitsi.util.event.*;

/**
 * Represents the telephony conference-related state of a <tt>Call</tt>.
 * Multiple <tt>Call</tt> instances share a single <tt>CallConference</tt>
 * instance when the former are into a telephony conference i.e. the local
 * peer/user is the conference focus. <tt>CallConference</tt> is
 * protocol-agnostic and thus enables cross-protocol conferences. Since a
 * non-conference <tt>Call</tt> may be converted into a conference <tt>Call</tt>
 * at any time, every <tt>Call</tt> instance maintains a <tt>CallConference</tt>
 * instance regardless of whether the <tt>Call</tt> in question is participating
 * in a telephony conference.
 *
 * @author Lyubomir Marinov
 */
public class CallConference
    extends PropertyChangeNotifier
{
    /**
     * The name of the <tt>CallConference</tt> property which specifies the list
     * of <tt>Call</tt>s participating in a telephony conference. A change in
     * the value of the property is delivered in the form of a
     * <tt>PropertyChangeEvent</tt> which has its <tt>oldValue</tt> or
     * <tt>newValue</tt> set to the <tt>Call</tt> which has been removed or
     * added to the list of <tt>Call</tt>s participating in the telephony
     * conference.
     */
    public static final String CALLS = "calls";

    /**
     * Gets the number of <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
     * participating in the telephony conference-related state of a specific
     * <tt>Call</tt>.
     *
     * @param call the <tt>Call</tt> for which the number of <tt>CallPeer</tt>s
     * associated with the <tt>Call</tt>s participating in its associated
     * telephony conference-related state
     * @return the number of <tt>CallPeer</tt>s associated with the
     * <tt>Call</tt>s participating in the telephony conference-related state
     * of the specified <tt>Call</tt>
     */
    public static int getCallPeerCount(Call call)
    {
        CallConference conference = call.getConference();

        /*
         * A Call instance is supposed to always maintain a CallConference
         * instance. Anyway, if it turns out that it is not the case, we will
         * consider the Call as a representation of a telephony conference.
         */
        return
            (conference == null)
                ? call.getCallPeerCount()
                : conference.getCallPeerCount();
    }

    /**
     * Gets a list of the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
     * participating in the telephony conference in which a specific
     * <tt>Call</tt> is participating.
     *
     * @param call the <tt>Call</tt> which specifies the telephony conference
     * the <tt>CallPeer</tt>s of which are to be retrieved
     * @return a list of the <tt>CallPeer</tt>s associated with the
     * <tt>Call</tt>s participating in the telephony conference in which the
     * specified <tt>call</tt> is participating
     */
    public static List<CallPeer> getCallPeers(Call call)
    {
        CallConference conference = call.getConference();
        List<CallPeer> callPeers = new ArrayList<CallPeer>();

        if (conference == null)
        {
            Iterator<? extends CallPeer> callPeerIt = call.getCallPeers();

            while (callPeerIt.hasNext())
                callPeers.add(callPeerIt.next());
        }
        else
            conference.getCallPeers(callPeers);
        return callPeers;
    }

    /**
     * Gets the list of <tt>Call</tt>s participating in the telephony conference
     * in which a specific <tt>Call</tt> is participating.
     *
     * @param call the <tt>Call</tt> which participates in the telephony
     * conference the list of participating <tt>Call</tt>s of which is to be
     * returned
     * @return the list of <tt>Call</tt>s participating in the telephony
     * conference in which the specified <tt>call</tt> is participating
     */
    public static List<Call> getCalls(Call call)
    {
        CallConference conference = call.getConference();
        List<Call> calls;

        if (conference == null)
            calls = Collections.emptyList();
        else
            calls = conference.getCalls();
        return calls;
    }

    /**
     * Determines whether a <tt>CallConference</tt> is to report the local
     * peer/user as a conference focus judging by a specific list of
     * <tt>Call</tt>s.
     *
     * @param calls the list of <tt>Call</tt> which are to be judged whether
     * the local peer/user that they represent is to be considered as a
     * conference focus
     * @return <tt>true</tt> if the local peer/user represented by the specified
     * <tt>calls</tt> is judged to be a conference focus; otherwise,
     * <tt>false</tt>
     */
    private static boolean isConferenceFocus(List<Call> calls)
    {
        int callCount = calls.size();
        boolean conferenceFocus;

        if (callCount < 1)
            conferenceFocus = false;
        else if (callCount > 1)
            conferenceFocus = true;
        else
            conferenceFocus = (calls.get(0).getCallPeerCount() > 1);
        return conferenceFocus;
    }

    /**
     * The <tt>CallChangeListener</tt> which listens to changes in the
     * <tt>Call</tt>s participating in this telephony conference.
     */
    private final CallChangeListener callChangeListener
        = new CallChangeListener()
        {
            @Override
            public void callPeerAdded(CallPeerEvent ev)
            {
                CallConference.this.onCallPeerEvent(ev);
            }

            @Override
            public void callPeerRemoved(CallPeerEvent ev)
            {
                CallConference.this.onCallPeerEvent(ev);
            }

            @Override
            public void callStateChanged(CallChangeEvent ev)
            {
                CallConference.this.callStateChanged(ev);
            }
        };

    /**
     * The list of <tt>CallChangeListener</tt>s added to the <tt>Call</tt>s
     * participating in this telephony conference via
     * {@link #addCallChangeListener(CallChangeListener)}.
     */
    private final List<CallChangeListener> callChangeListeners
        = new LinkedList<CallChangeListener>();

    /**
     * The <tt>CallPeerConferenceListener</tt> which listens to the
     * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in
     * this telephony conference.
     */
    private final CallPeerConferenceListener callPeerConferenceListener
        = new CallPeerConferenceAdapter()
        {
            /**
             * {@inheritDoc}
             *
             * Invokes
             * {@link CallConference#onCallPeerConferenceEvent(
             * CallPeerConferenceEvent)}.
             */
            @Override
            protected void onCallPeerConferenceEvent(CallPeerConferenceEvent ev)
            {
                CallConference.this.onCallPeerConferenceEvent(ev);
            }
            
            /**
             * {@inheritDoc}
             *
             * Invokes
             * {@link CallConference#onCallPeerConferenceEvent(
             * CallPeerConferenceEvent)}.
             */
            @Override
            public void conferenceMemberErrorReceived(
                CallPeerConferenceEvent ev)
            {
                CallConference.this.onCallPeerConferenceEvent(ev);
            }
        };

    /**
     * The list of <tt>CallPeerConferenceListener</tt>s added to the
     * <tt>CallPeer</tt>s associated with the <tt>CallPeer</tt>s participating
     * in this telephony conference via
     * {@link #addCallPeerConferenceListener}.
     */
    private final List<CallPeerConferenceListener> callPeerConferenceListeners
        = new LinkedList<CallPeerConferenceListener>();

    /**
     * The synchronization root/<tt>Object</tt> which protects the access to
     * {@link #immutableCalls} and {@link #mutableCalls}.
     */
    private final Object callsSyncRoot = new Object();

    /**
     * The indicator which determines whether the local peer represented by this
     * instance and the <tt>Call</tt>s participating in it is acting as a
     * conference focus. The SIP protocol, for example, will add the
     * &quot;isfocus&quot; parameter to the Contact headers of its outgoing
     * signaling if <tt>true</tt>.
     */
    private boolean conferenceFocus = false;

    /**
     * The list of <tt>Call</tt>s participating in this telephony conference as
     * an immutable <tt>List</tt> which can be exposed out of this instance
     * without the need to make a copy. In other words, it is an unmodifiable
     * view of {@link #mutableCalls}.
     */
    private List<Call> immutableCalls;

    /**
     * The indicator which determines whether the telephony conference
     * represented by this instance is utilizing the Jitsi Videobridge
     * server-side telephony conferencing technology.
     */
    private final boolean jitsiVideobridge;

    /**
     * The list of <tt>Call</tt>s participating in this telephony conference as
     * a mutable <tt>List</tt> which should not be exposed out of this instance.
     */
    private List<Call> mutableCalls;

    /**
     * Initializes a new <tt>CallConference</tt> instance.
     */
    public CallConference()
    {
        this(false);
    }

    /**
     * Initializes a new <tt>CallConference</tt> instance which is to optionally
     * utilize the Jitsi Videobridge server-side telephony conferencing
     * technology.
     *
     * @param jitsiVideobridge <tt>true</tt> if the telephony conference
     * represented by the new instance is to utilize the Jitsi Videobridge
     * server-side telephony conferencing technology; otherwise, <tt>false</tt>
     */
    public CallConference(boolean jitsiVideobridge)
    {
        this.jitsiVideobridge = jitsiVideobridge;

        mutableCalls = new ArrayList<Call>();
        immutableCalls = Collections.unmodifiableList(mutableCalls);
    }

    /**
     * Adds a specific <tt>Call</tt> to the list of <tt>Call</tt>s participating
     * in this telephony conference.
     *
     * @param call the <tt>Call</tt> to add to the list of <tt>Call</tt>s
     * participating in this telephony conference
     * @return <tt>true</tt> if the list of <tt>Call</tt>s participating in this
     * telephony conference changed as a result of the method call; otherwise,
     * <tt>false</tt>
     * @throws NullPointerException if <tt>call</tt> is <tt>null</tt>
     */
    boolean addCall(Call call)
    {
        if (call == null)
            throw new NullPointerException("call");

        synchronized (callsSyncRoot)
        {
            if (mutableCalls.contains(call))
                return false;

            /*
             * Implement the List of Calls participating in this telephony
             * conference as a copy-on-write storage in order to optimize the
             * getCalls method which is likely to be executed much more often
             * than the addCall and removeCall methods.
             */
            List<Call> newMutableCalls = new ArrayList<Call>(mutableCalls);

            if (newMutableCalls.add(call))
            {
                mutableCalls = newMutableCalls;
                immutableCalls = Collections.unmodifiableList(mutableCalls);
            }
            else
                return false;
        }

        callAdded(call);
        return true;
    }

    /**
     * Adds a <tt>CallChangeListener</tt> to the <tt>Call</tt>s participating in
     * this telephony conference. The method is a convenience that takes on the
     * responsibility of tracking the <tt>Call</tt>s that get added/removed
     * to/from this telephony conference.
     *
     * @param listener the <tt>CallChangeListner</tt> to be added to the
     * <tt>Call</tt>s participating in this telephony conference
     * @throws NullPointerException if <tt>listener</tt> is <tt>null</tt>
     */
    public void addCallChangeListener(CallChangeListener listener)
    {
        if (listener == null)
            throw new NullPointerException("listener");
        else
        {
            synchronized (callChangeListeners)
            {
                if (!callChangeListeners.contains(listener))
                    callChangeListeners.add(listener);
            }
        }
    }

    /**
     * Adds {@link #callPeerConferenceListener} to the <tt>CallPeer</tt>s
     * associated with a specific <tt>Call</tt>.
     *
     * @param call the <tt>Call</tt> to whose associated <tt>CallPeer</tt>s
     * <tt>callPeerConferenceListener</tt> is to be added
     */
    private void addCallPeerConferenceListener(Call call)
    {
        Iterator<? extends CallPeer> callPeerIter = call.getCallPeers();

        while (callPeerIter.hasNext())
        {
            callPeerIter.next().addCallPeerConferenceListener(
                    callPeerConferenceListener);
        }
    }

    /**
     * Adds a <tt>CallPeerConferenceListener</tt> to the <tt>CallPeer</tt>s
     * associated with the <tt>Call</tt>s participating in this telephony
     * conference. The method is a convenience that takes on the responsibility
     * of tracking the <tt>Call</tt>s that get added/removed to/from this
     * telephony conference and the <tt>CallPeer</tt> that get added/removed
     * to/from these <tt>Call</tt>s.
     *
     * @param listener the <tt>CallPeerConferenceListener</tt> to be added to
     * the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating
     * in this telephony conference
     * @throws NullPointerException if <tt>listener</tt> is <tt>null</tt>
     */
    public void addCallPeerConferenceListener(
            CallPeerConferenceListener listener)
    {
        if (listener == null)
            throw new NullPointerException("listener");
        else
        {
            synchronized (callPeerConferenceListeners)
            {
                if (!callPeerConferenceListeners.contains(listener))
                    callPeerConferenceListeners.add(listener);
            }
        }
    }

    /**
     * Notifies this <tt>CallConference</tt> that a specific <tt>Call</tt> has
     * been added to the list of <tt>Call</tt>s participating in this telephony
     * conference.
     *
     * @param call the <tt>Call</tt> which has been added to the list of
     * <tt>Call</tt>s participating in this telephony conference
     */
    protected void callAdded(Call call)
    {
        call.addCallChangeListener(callChangeListener);
        addCallPeerConferenceListener(call);

        /*
         * Update the conferenceFocus state. Because the public
         * setConferenceFocus method allows forcing a specific value on the
         * state in question and because it does not sound right to have the
         * adding of a Call set conferenceFocus to false, only update it if the
         * new conferenceFocus value is true,
         */
        boolean conferenceFocus = isConferenceFocus(getCalls());

        if (conferenceFocus)
            setConferenceFocus(conferenceFocus);

        firePropertyChange(CALLS, null, call);
    }

    /**
     * Notifies this <tt>CallConference</tt> that a specific <tt>Call</tt> has
     * been removed from the list of <tt>Call</tt>s participating in this
     * telephony conference.
     *
     * @param call the <tt>Call</tt> which has been removed from the list of
     * <tt>Call</tt>s participating in this telephony conference
     */
    protected void callRemoved(Call call)
    {
        call.removeCallChangeListener(callChangeListener);
        removeCallPeerConferenceListener(call);

        /*
         * Update the conferenceFocus state. Following the line of thinking
         * expressed in the callAdded method, only update it if the new
         * conferenceFocus value is false.
         */
        boolean conferenceFocus = isConferenceFocus(getCalls());

        if (!conferenceFocus)
            setConferenceFocus(conferenceFocus);

        firePropertyChange(CALLS, call, null);
    }

    /**
     * Notifies this telephony conference that the <tt>CallState</tt> of a
     * <tt>Call</tt> has changed.
     *
     * @param ev a <tt>CallChangeEvent</tt> which specifies the <tt>Call</tt>
     * which had its <tt>CallState</tt> changed and the old and new
     * <tt>CallState</tt>s of that <tt>Call</tt>
     */
    private void callStateChanged(CallChangeEvent ev)
    {
        Call call = ev.getSourceCall();

        if (containsCall(call))
        {
            try
            {
                // Forward the CallChangeEvent to the callChangeListeners.
                for (CallChangeListener l : getCallChangeListeners())
                    l.callStateChanged(ev);
            }
            finally
            {
                if (CallChangeEvent.CALL_STATE_CHANGE
                        .equals(ev.getPropertyName())
                    && CallState.CALL_ENDED.equals(ev.getNewValue()))
                {
                    /*
                     * Should not be vital because Call will remove itself.
                     * Anyway, do it for the sake of completeness.
                     */
                    removeCall(call);
                }
            }
        }
    }

    /**
     * Notifies this <tt>CallConference</tt> that the value of its
     * <tt>conferenceFocus</tt> property has changed from a specific old value
     * to a specific new value.
     *
     * @param oldValue the value of the <tt>conferenceFocus</tt> property of
     * this instance before the change
     * @param newValue the value of the <tt>conferenceFocus</tt> property of
     * this instance after the change
     */
    protected void conferenceFocusChanged(boolean oldValue, boolean newValue)
    {
        firePropertyChange(Call.CONFERENCE_FOCUS, oldValue, newValue);
    }

    /**
     * Determines whether a specific <tt>Call</tt> is participating in this
     * telephony conference.
     *
     * @param call the <tt>Call</tt> which is to be checked whether it is
     * participating in this telephony conference
     * @return <tt>true</tt> if the specified <tt>call</tt> is participating in
     * this telephony conference
     */
    public boolean containsCall(Call call)
    {
        synchronized (callsSyncRoot)
        {
            return mutableCalls.contains(call);
        }
    }

    /**
     * Gets the list of <tt>CallChangeListener</tt>s added to the <tt>Call</tt>s
     * participating in this telephony conference via
     * {@link #addCallChangeListener(CallChangeListener)}.
     *
     * @return the list of <tt>CallChangeListener</tt>s added to the
     * <tt>Call</tt>s participating in this telephony conference via
     * {@link #addCallChangeListener(CallChangeListener)}
     */
    private CallChangeListener[] getCallChangeListeners()
    {
        synchronized (callChangeListeners)
        {
            return
                callChangeListeners.toArray(
                        new CallChangeListener[callChangeListeners.size()]);
        }
    }

    /**
     * Gets the number of <tt>Call</tt>s that are participating in this
     * telephony conference.
     *
     * @return the number of <tt>Call</tt>s that are participating in this
     * telephony conference
     */
    public int getCallCount()
    {
        synchronized (callsSyncRoot)
        {
            return mutableCalls.size();
        }
    }

    /**
     * Gets the list of <tt>CallPeerConferenceListener</tt>s added to the
     * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in
     * this telephony conference via
     * {@link #addCallPeerConferenceListener(CallPeerConferenceListener)}.
     *
     * @return the list of <tt>CallPeerConferenceListener</tt>s added to the
     * <tt>CallPeer</tt>s associated with the <tt>Call</tt>s participating in
     * this telephony conference via
     * {@link #addCallPeerConferenceListener(CallPeerConferenceListener)}
     */
    private CallPeerConferenceListener[] getCallPeerConferenceListeners()
    {
        synchronized (callPeerConferenceListeners)
        {
            return
                callPeerConferenceListeners.toArray(
                        new CallPeerConferenceListener[
                                callPeerConferenceListeners.size()]);
        }
    }

    /**
     * Gets the number of <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
     * participating in this telephony conference.
     *
     * @return the number of <tt>CallPeer</tt>s associated with the
     * <tt>Call</tt>s participating in this telephony conference
     */
    public int getCallPeerCount()
    {
        int callPeerCount = 0;

        for (Call call : getCalls())
            callPeerCount += call.getCallPeerCount();
        return callPeerCount;
    }

    /**
     * Gets a list of the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
     * participating in this telephony conference.
     *
     * @return a list of the <tt>CallPeer</tt>s associated with the
     * <tt>Call</tt>s participating in this telephony conference
     */
    public List<CallPeer> getCallPeers()
    {
        List<CallPeer> callPeers = new ArrayList<CallPeer>();

        getCallPeers(callPeers);
        return callPeers;
    }

    /**
     * Adds the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
     * participating in this telephony conference into a specific <tt>List</tt>.
     *
     * @param callPeers a <tt>List</tt> into which the <tt>CallPeer</tt>s
     * associated with the <tt>Call</tt>s participating in this telephony
     * conference are to be added
     */
    protected void getCallPeers(List<CallPeer> callPeers)
    {
        for (Call call : getCalls())
        {
            Iterator<? extends CallPeer> callPeerIt = call.getCallPeers();

            while (callPeerIt.hasNext())
                callPeers.add(callPeerIt.next());
        }
    }

    /**
     * Gets the list of <tt>Call</tt> participating in this telephony
     * conference.
     *
     * @return the list of <tt>Call</tt>s participating in this telephony
     * conference. An empty array of <tt>Call</tt> element type is returned if
     * there are no <tt>Call</tt>s in this telephony conference-related state.
     */
    public List<Call> getCalls()
    {
        synchronized (callsSyncRoot)
        {
            return immutableCalls;
        }
    }

    /**
     * Determines whether the local peer/user associated with this instance and
     * represented by the <tt>Call</tt>s participating into it is acting as a
     * conference focus.
     *
     * @return <tt>true</tt> if the local peer/user associated by this instance
     * is acting as a conference focus; otherwise, <tt>false</tt>
     */
    public boolean isConferenceFocus()
    {
        return conferenceFocus;
    }

    /**
     * Determines whether the current state of this instance suggests that the
     * telephony conference it represents has ended. Iterates over the
     * <tt>Call</tt>s participating in this telephony conference and looks for a
     * <tt>Call</tt> which is not in the {@link CallState#CALL_ENDED} state.
     *
     * @return <tt>true</tt> if the current state of this instance suggests that
     * the telephony conference it represents has ended; otherwise,
     * <tt>false</tt>
     */
    public boolean isEnded()
    {
        for (Call call : getCalls())
        {
            if (!CallState.CALL_ENDED.equals(call.getCallState()))
                return false;
        }
        return true;
    }

    /**
     * Determines whether the telephony conference represented by this instance
     * is utilizing the Jitsi Videobridge server-side telephony conferencing
     * technology.
     *
     * @return <tt>true</tt> if the telephony conference represented by this
     * instance is utilizing the Jitsi Videobridge server-side telephony
     * conferencing technology
     */
    public boolean isJitsiVideobridge()
    {
        return jitsiVideobridge;
    }

    /**
     * Notifies this telephony conference that a
     * <tt>CallPeerConferenceEvent</tt> was fired by a <tt>CallPeer</tt>
     * associated with a <tt>Call</tt> participating in this telephony
     * conference. Forwards the specified <tt>CallPeerConferenceEvent</tt> to
     * {@link #callPeerConferenceListeners}.
     *
     * @param ev the <tt>CallPeerConferenceEvent</tt> which was fired
     */
    private void onCallPeerConferenceEvent(CallPeerConferenceEvent ev)
    {
        int eventID = ev.getEventID();

        for (CallPeerConferenceListener l : getCallPeerConferenceListeners())
        {
            switch (eventID)
            {
            case CallPeerConferenceEvent.CONFERENCE_FOCUS_CHANGED:
                l.conferenceFocusChanged(ev);
                break;
            case CallPeerConferenceEvent.CONFERENCE_MEMBER_ADDED:
                l.conferenceMemberAdded(ev);
                break;
            case CallPeerConferenceEvent.CONFERENCE_MEMBER_REMOVED:
                l.conferenceMemberRemoved(ev);
                break;
            case CallPeerConferenceEvent.CONFERENCE_MEMBER_ERROR_RECEIVED:
                l.conferenceMemberErrorReceived(ev);
                break;
            default:
                throw new UnsupportedOperationException(
                        "Unsupported CallPeerConferenceEvent eventID.");
            }
        }
    }

    /**
     * Notifies this telephony conference about a specific
     * <tt>CallPeerEvent</tt> i.e. that a <tt>CallPeer</tt> was either added to
     * or removed from a <tt>Call</tt>.
     *
     * @param ev a <tt>CallPeerEvent</tt> which specifies the <tt>CallPeer</tt>
     * which was added or removed and the <tt>Call</tt> to which it was added or
     * from which is was removed
     */
    private void onCallPeerEvent(CallPeerEvent ev)
    {
        Call call = ev.getSourceCall();

        if (containsCall(call))
        {
            /*
             * Update the conferenceFocus state. Following the line of thinking
             * expressed in the callAdded and callRemoved methods, only update
             * it if the new conferenceFocus value is in accord with the
             * expectations.
             */
            int eventID = ev.getEventID();
            boolean conferenceFocus = isConferenceFocus(getCalls());

            switch (eventID)
            {
            case CallPeerEvent.CALL_PEER_ADDED:
                if (conferenceFocus)
                    setConferenceFocus(conferenceFocus);
                break;
            case CallPeerEvent.CALL_PEER_REMOVED:
                if (!conferenceFocus)
                    setConferenceFocus(conferenceFocus);
                break;
            default:
                /*
                 * We're interested in the adding and removing of CallPeers
                 * only.
                 */
                break;
            }

            try
            {
                // Forward the CallPeerEvent to the callChangeListeners.
                for (CallChangeListener l : getCallChangeListeners())
                {
                    switch (eventID)
                    {
                    case CallPeerEvent.CALL_PEER_ADDED:
                        l.callPeerAdded(ev);
                        break;
                    case CallPeerEvent.CALL_PEER_REMOVED:
                        l.callPeerRemoved(ev);
                        break;
                    default:
                        break;
                    }
                }
            }
            finally
            {
                /*
                 * Add/remove the callPeerConferenceListener to/from the source
                 * CallPeer (for the purposes of the
                 * addCallPeerConferenceListener method of this CallConference).
                 */
                CallPeer callPeer = ev.getSourceCallPeer();

                switch (eventID)
                {
                case CallPeerEvent.CALL_PEER_ADDED:
                    callPeer.addCallPeerConferenceListener(
                            callPeerConferenceListener);
                    break;
                case CallPeerEvent.CALL_PEER_REMOVED:
                    callPeer.removeCallPeerConferenceListener(
                            callPeerConferenceListener);
                    break;
                default:
                    break;
                }
            }
        }
    }

    /**
     * Removes a specific <tt>Call</tt> from the list of <tt>Call</tt>s
     * participating in this telephony conference.
     *
     * @param call the <tt>Call</tt> to remove from the list of <tt>Call</tt>s
     * participating in this telephony conference
     * @return <tt>true</tt> if the list of <tt>Call</tt>s participating in this
     * telephony conference changed as a result of the method call; otherwise,
     * <tt>false</tt>
     */
    boolean removeCall(Call call)
    {
        if (call == null)
            return false;

        synchronized (callsSyncRoot)
        {
            if (!mutableCalls.contains(call))
                return false;

            /*
             * Implement the List of Calls participating in this telephony
             * conference as a copy-on-write storage in order to optimize the
             * getCalls method which is likely to be executed much more often
             * than the addCall and removeCall methods.
             */
            List<Call> newMutableCalls = new ArrayList<Call>(mutableCalls);

            if (newMutableCalls.remove(call))
            {
                mutableCalls = newMutableCalls;
                immutableCalls = Collections.unmodifiableList(mutableCalls);
            }
            else
                return false;
        }

        callRemoved(call);
        return true;
    }

    /**
     * Removes a <tt>CallChangeListener</tt> from the <tt>Call</tt>s
     * participating in this telephony conference.
     *
     * @param listener the <tt>CallChangeListener</tt> to be removed from the
     * <tt>Call</tt>s participating in this telephony conference
     * @see #addCallChangeListener(CallChangeListener)
     */
    public void removeCallChangeListener(CallChangeListener listener)
    {
        if (listener != null)
        {
            synchronized (callChangeListeners)
            {
                callChangeListeners.remove(listener);
            }
        }
    }

    /**
     * Removes {@link #callPeerConferenceListener} from the <tt>CallPeer</tt>s
     * associated with a specific <tt>Call</tt>.
     *
     * @param call the <tt>Call</tt> from whose associated <tt>CallPeer</tt>s
     * <tt>callPeerConferenceListener</tt> is to be removed
     */
    private void removeCallPeerConferenceListener(Call call)
    {
        Iterator<? extends CallPeer> callPeerIter = call.getCallPeers();

        while (callPeerIter.hasNext())
        {
            callPeerIter.next().removeCallPeerConferenceListener(
                    callPeerConferenceListener);
        }
    }

    /**
     * Removes a <tt>CallPeerConferenceListener</tt> from the <tt>CallPeer</tt>s
     * associated with the <tt>Call</tt>s participating in this telephony
     * conference.
     *
     * @param listener the <tt>CallPeerConferenceListener</tt> to be removed
     * from the <tt>CallPeer</tt>s associated with the <tt>Call</tt>s
     * participating in this telephony conference
     * @see #addCallPeerConferenceListener(CallPeerConferenceListener)
     */
    public void removeCallPeerConferenceListener(
            CallPeerConferenceListener listener)
    {
        if (listener != null)
        {
            synchronized (callPeerConferenceListeners)
            {
                callPeerConferenceListeners.remove(listener);
            }
        }
    }

    /**
     * Sets the indicator which determines whether the local peer represented by
     * this instance and the <tt>Call</tt>s participating in it is acting as a
     * conference focus (and thus may, for example, need to send the
     * corresponding parameters in its outgoing signaling).
     *
     * @param conferenceFocus <tt>true</tt> if the local peer represented by
     * this instance and the <tt>Call</tt>s participating in it is to act as a
     * conference focus; otherwise, <tt>false</tt>
     */
    public void setConferenceFocus(boolean conferenceFocus)
    {
        if (this.conferenceFocus != conferenceFocus)
        {
            boolean oldValue = isConferenceFocus();

            this.conferenceFocus = conferenceFocus;

            boolean newValue = isConferenceFocus();

            if (oldValue != newValue)
                conferenceFocusChanged(oldValue, newValue);
        }
    }
}
