/*
 * 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.impl.gui.main.call.conference;

import java.awt.*;
import java.util.*;
import java.util.List;

import javax.swing.*;

import net.java.sip.communicator.impl.gui.main.call.*;
import net.java.sip.communicator.impl.gui.utils.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.util.*;
import net.java.sip.communicator.plugin.desktoputil.*;
import net.java.sip.communicator.plugin.desktoputil.TransparentPanel;

import org.jitsi.util.swing.*;

/**
 * Extends <tt>BasicConferenceCallPanel</tt> to implement a user interface
 * <tt>Component</tt> which depicts a <tt>CallConference</tt> with audio and
 * video and is contained in a <tt>CallPanel</tt>.
 *
 * @author Yana Stamcheva
 * @author Lyubomir Marinov
 */
public class VideoConferenceCallPanel
    extends BasicConferenceCallPanel
{
    /**
     * The <tt>Logger</tt> used by the <tt>VideoConferenceCallPanel</tt> class
     * and its instances for logging output.
     */
    private static final Logger logger
        = Logger.getLogger(VideoConferenceCallPanel.class);

    /**
     * The compile-time flag which indicates whether each video displayed by
     * <tt>VideoConferenceCallPanel</tt> is to be depicted with an associated
     * tool bar showing information and controls related to the (local or
     * remote) peer sending the respective video.
     */
    private static final boolean SHOW_TOOLBARS = true;

    /**
     * The facility which aids this instance with the video-related information.
     */
    private final UIVideoHandler2 uiVideoHandler;

    /**
     * The <tt>Observer</tt> which listens to {@link #uiVideoHandler} about
     * changes in the video-related information.
     */
    private final Observer uiVideoHandlerObserver
        = new Observer()
        {
            public void update(Observable o, Object arg)
            {
                updateViewFromModel();
            }
        };

    /**
     * The <tt>VideoContainer</tt> which occupies this whole <tt>Component</tt>
     * and arranges the visual <tt>Component</tt>s displaying the video
     * streaming between the local peer/user and the remote peer(s).
     */
    private final VideoContainer videoContainer;

    /**
     * The set of visual <tt>Component</tt>s displaying video streaming between
     * the local peer/user and the remote peers which are depicted by this
     * instance.
     */
    private final Set<Component> videos = new HashSet<Component>();

    /**
     * The thumbnail container.
     */
    private final ThumbnailConferenceCallPanel thumbnailContainer;

    private final JPanel thumbnailPanel;

    /**
     * Initializes a new <tt>VideoConferenceCallPanel</tt> instance which is to
     * be used by a specific <tt>CallPanel</tt> to depict a specific
     * <tt>CallConference</tt>. The new instance will depict both the
     * audio-related and the video-related information.
     *
     * @param callPanel the <tt>CallPanel</tt> which will use the new instance
     * to depict the specified <tt>CallConference</tt>.
     * @param callConference the <tt>CallConference</tt> to be depicted by the
     * new instance
     * @param uiVideoHandler the utility which is to aid the new instance in
     * dealing with the video-related information
     */
    public VideoConferenceCallPanel(
            CallPanel callPanel,
            CallConference callConference,
            UIVideoHandler2 uiVideoHandler)
    {
        super(callPanel, callConference);

        this.uiVideoHandler = uiVideoHandler;

        thumbnailPanel = new JPanel(new BorderLayout());
        thumbnailContainer
            = new ThumbnailConferenceCallPanel( callPanel,
                                                callConference,
                                                uiVideoHandler);

        videoContainer = createVideoContainer();

        /*
         * Our user interface hierarchy has been initialized so we are ready to
         * begin receiving events warranting updates of this view from its
         * model.
         */
        uiVideoHandler.addObserver(uiVideoHandlerObserver);

        /*
         * Notify the super that this instance has completed its initialization
         * and the view that it implements is ready to be updated from the
         * model.
         */
        initializeComplete();
    }

    private void addConferenceMemberContainers(
            ConferenceParticipantContainer cpc)
    {
        List<ConferenceParticipantContainer> cmcs
            = cpc.conferenceMemberContainers;

        if ((cmcs != null) && !cmcs.isEmpty())
        {
            for (ConferenceParticipantContainer cmc : cmcs)
            {
                if (!cmc.toBeRemoved)
                {
                    videoContainer.add(
                            cmc.getComponent(),
                            VideoLayout.CENTER_REMOTE);
                }
            }
        }
    }

    private Component createDefaultPhotoPanel(Call call)
    {
        OperationSetServerStoredAccountInfo accountInfo
            = call.getProtocolProvider().getOperationSet(
                    OperationSetServerStoredAccountInfo.class);
        ImageIcon photoLabelIcon = null;

        if (accountInfo != null)
        {
            byte[] accountImage = AccountInfoUtils.getImage(accountInfo);

            // do not set empty images
            if ((accountImage != null) && (accountImage.length > 0))
                photoLabelIcon = new ImageIcon(accountImage);
        }
        if (photoLabelIcon == null)
        {
            photoLabelIcon
                = new ImageIcon(
                        ImageLoader.getImage(ImageLoader.DEFAULT_USER_PHOTO));
        }

        return createDefaultPhotoPanel(photoLabelIcon);
    }

    private Component createDefaultPhotoPanel(CallPeer callPeer)
    {
        byte[] peerImage = CallManager.getPeerImage(callPeer);
        ImageIcon photoLabelIcon
            = (peerImage == null)
                ? new ImageIcon(
                        ImageLoader.getImage(ImageLoader.DEFAULT_USER_PHOTO))
                : new ImageIcon(peerImage);

        return createDefaultPhotoPanel(photoLabelIcon);
    }

    private Component createDefaultPhotoPanel(ConferenceMember conferenceMember)
    {
        return
            createDefaultPhotoPanel(
                    new ImageIcon(
                            ImageLoader.getImage(
                                    ImageLoader.DEFAULT_USER_PHOTO)));
    }

    /**
     * Creates a new <tt>Component</tt> which is to display a specific
     * <tt>ImageIcon</tt> representing the photo of a participant in a call.
     *
     * @param photoLabelIcon the <tt>ImageIcon</tt> which represents the photo
     * of a participant in a call and which is to be displayed by the new
     * <tt>Component</tt>
     * @return a new <tt>Component</tt> which displays the specified
     * <tt>photoLabelIcon</tt>
     */
    private Component createDefaultPhotoPanel(ImageIcon photoLabelIcon)
    {
        JLabel photoLabel = new JLabel();

        photoLabel.setIcon(photoLabelIcon);

        @SuppressWarnings("serial")
        JPanel photoPanel
            = new TransparentPanel(new GridBagLayout())
            {
                /**
                 * @{inheritDoc}
                 */
                @Override
                public void paintComponent(Graphics g)
                {
                    super.paintComponent(g);

                    g = g.create();
                    try
                    {
                        AntialiasingManager.activateAntialiasing(g);

                        g.setColor(Color.GRAY);
                        g.fillRoundRect(
                                0, 0, this.getWidth(), this.getHeight(),
                                6, 6);
                    }
                    finally
                    {
                        g.dispose();
                    }
                }
            };

        photoPanel.setPreferredSize(new Dimension(320, 240));

        GridBagConstraints photoPanelConstraints = new GridBagConstraints();

        photoPanelConstraints.anchor = GridBagConstraints.CENTER;
        photoPanelConstraints.fill = GridBagConstraints.NONE;
        photoPanel.add(photoLabel, photoPanelConstraints);

        return photoPanel;
    }

    /**
     * Initializes a new <tt>VideoContainer</tt> instance which is to contain
     * the visual/video <tt>Component</tt>s of the telephony conference depicted
     * by this instance.
     */
    private VideoContainer createVideoContainer()
    {
        VideoContainer videoContainer = new VideoContainer(null, true);

        thumbnailPanel.setBackground(Color.DARK_GRAY);
        thumbnailPanel.add(thumbnailContainer, BorderLayout.NORTH);

        add(thumbnailPanel, BorderLayout.EAST);
        add(videoContainer, BorderLayout.CENTER);

        return videoContainer;
    }

    /**
     * Shows/hides the participants thumbnails list.
     *
     * @param show <tt>true</tt> to show the participants list, <tt>false</tt>
     * to hide it
     */
    public void showThumbnailsList(boolean show)
    {
        thumbnailPanel.setVisible(show);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void dispose()
    {
        try
        {
            uiVideoHandler.deleteObserver(uiVideoHandlerObserver);
        }
        finally
        {
            super.dispose();
        }
    }

    /**
     * Determines whether a specific <tt>ConferenceMember</tt> represents the
     * same conference participant as a specific <tt>CallPeer</tt>. If the
     * specified <tt>conferenceMember</tt> is <tt>null</tt>, returns
     * <tt>true</tt>. Otherwise, determines whether the addresses of the
     * specified <tt>conferenceMember</tt> and the specified <tt>callPeer</tt>
     * identify one and the same entity.
     *
     * @param conferenceMember the <tt>ConferenceMember</tt> to be checked
     * whether is represents the same conference participant as the specified
     * <tt>callPeer</tt>. If it is <tt>null</tt>, <tt>true</tt> is returned.
     * @param callPeer the <tt>CallPeer</tt> to be checked whether it represents
     * the same conference participant as the specified
     * <tt>conferenceMember</tt>
     * @return <tt>true</tt> if the specified <tt>conferenceMember</tt> and the
     * specified <tt>callPeer</tt> represent the same conference participant or
     * the specified <tt>conferenceMember</tt> is <tt>null</tt>; otherwise,
     * <tt>false</tt>
     */
    private boolean isConferenceMemberCallPeer(
            ConferenceMember conferenceMember,
            CallPeer callPeer)
    {
        return
            (conferenceMember == null)
                ? true
                : CallManager.addressesAreEqual(
                        conferenceMember.getAddress(),
                        callPeer.getAddress());
    }

    /**
     * Determines whether a specific <tt>ConferenceMember</tt> represents the
     * local peer/user. Since this instance depicts a whole telephony
     * conference, the local peer/user may be participating with multiple
     * <tt>Call</tt>s in it. The <tt>Call</tt>s may be through different
     * (local) accounts. That's why the implementation determines whether the
     * address of the specified <tt>conferenceMember</tt> identifies the address
     * of a (local) accounts involved in the telephony conference depicted by
     * this instance.
     *
     * @param conferenceMember the <tt>ConferenceMember</tt> to be checked
     * whether it represents the local peer/user
     * @return <tt>true</tt> if the specified <tt>conferenceMember</tt>
     * represents the local peer/user; otherwise, <tt>false</tt>
     */
    private boolean isConferenceMemberLocalUser(
            ConferenceMember conferenceMember)
    {
        String address = conferenceMember.getAddress();

        for (Call call : callConference.getCalls())
        {
            if (CallManager.addressesAreEqual(
                    address,
                    call.getProtocolProvider().getAccountID()
                            .getAccountAddress()))
            {
                return true;
            }
        }
        return false;
    }

    private void removeConferenceMemberContainers(
            ConferenceParticipantContainer cpc,
            boolean all)
    {
        List<ConferenceParticipantContainer> cmcs
            = cpc.conferenceMemberContainers;

        if ((cmcs != null) && !cmcs.isEmpty())
        {
            Iterator<ConferenceParticipantContainer> i = cmcs.iterator();

            while (i.hasNext())
            {
                ConferenceParticipantContainer cmc = i.next();

                if (all || cmc.toBeRemoved)
                {
                    i.remove();

                    videoContainer.remove(cmc.getComponent());
                    cmc.dispose();
                }
            }
        }
    }

    /**
     * Updates the <tt>ConferenceParticipantContainer</tt>s which depict the
     * <tt>ConferenceMember</tt>s of the <tt>CallPeer</tt> depicted by a
     * specific <tt>ConferenceParticipantContainer</tt>.
     *
     * @param cpc the <tt>ConferenceParticipantContainer</tt> which depicts the
     * <tt>CallPeer</tt> whose <tt>ConferenceMember</tt>s are to be depicted
     * @param videos the visual <tt>Component</tt>s displaying video streaming
     * from the remote peer (represented by <tt>cpc</tt>) to the local peer/user
     * @param videoTelephony the <tt>OperationSetVideoTelephony</tt> which
     * retrieved the specified <tt>videos</tt> from the <tt>CallPeer</tt>
     * depicted by <tt>cpc</tt>. While the <tt>CallPeer</tt> could be queried
     * for it, such a query would waste more resources at run time given that
     * the invoker has it already.
     */
    private void updateConferenceMemberContainers(
            ConferenceParticipantContainer cpc,
            List<Component> videos,
            OperationSetVideoTelephony videoTelephony)
    {
        CallPeer callPeer = (CallPeer) cpc.getParticipant();
        List<ConferenceParticipantContainer> cmcs
            = cpc.conferenceMemberContainers;

        /*
         * Invalidate all conferenceMemberContainers. Then validate which of
         * them are to remain and which of them are to really be removed
         * later on.
         */
        if (cmcs != null)
        {
            for (ConferenceParticipantContainer cmc : cmcs)
                cmc.toBeRemoved = true;
        }

        /*
         * Depict the remote videos. They may or may not be associated with
         * ConferenceMembers so the ConferenceMembers which have no
         * associated videos will be depicted afterwards.
         */
        if (videos != null)
        {
            Component video = cpc.getVideo();

            for (Component conferenceMemberVideo : videos)
            {
                /*
                 * One of the remote videos is already used to depict the
                 * callPeer.
                 */
                if (conferenceMemberVideo == video)
                    continue;

                boolean addNewConferenceParticipantContainer = true;
                ConferenceMember conferenceMember
                    = videoTelephony.getConferenceMember(
                            callPeer,
                            conferenceMemberVideo);

                if (cmcs == null)
                {
                    cmcs = new LinkedList<ConferenceParticipantContainer>();
                    cpc.conferenceMemberContainers = cmcs;
                }
                else
                {
                    for (ConferenceParticipantContainer cmc : cmcs)
                    {
                        Object cmcParticipant = cmc.getParticipant();

                        if (conferenceMember == null)
                        {
                            if (cmcParticipant == callPeer)
                            {
                                Component cmcVideo = cmc.getVideo();

                                if (cmcVideo == null)
                                {
                                    cmc.setVideo(conferenceMemberVideo);
                                    cmc.toBeRemoved = false;
                                    addNewConferenceParticipantContainer
                                        = false;
                                    break;
                                }
                                else if (cmcVideo == conferenceMemberVideo)
                                {
                                    cmc.toBeRemoved = false;
                                    addNewConferenceParticipantContainer
                                        = false;
                                    break;
                                }
                            }
                        }
                        else if (cmcParticipant == conferenceMember)
                        {
                            cmc.setVideo(conferenceMemberVideo);
                            cmc.toBeRemoved = false;
                            addNewConferenceParticipantContainer = false;
                            break;
                        }
                    }
                }

                if (addNewConferenceParticipantContainer)
                {
                    ConferenceParticipantContainer cmc
                        = (conferenceMember == null)
                            ? new ConferenceParticipantContainer(
                                    callPeer,
                                    conferenceMemberVideo)
                            : new ConferenceParticipantContainer(
                                    conferenceMember,
                                    conferenceMemberVideo);

                    cmcs.add(cmc);
                }
            }
        }

        /*
         * Depict the ConferenceMembers which have not been depicted yet.
         * They have no associated videos.
         */
        List<ConferenceMember> conferenceMembers
            = callPeer.getConferenceMembers();

        if (!conferenceMembers.isEmpty())
        {
            if (cmcs == null)
            {
                cmcs = new LinkedList<ConferenceParticipantContainer>();
                cpc.conferenceMemberContainers = cmcs;
            }
            for (ConferenceMember conferenceMember : conferenceMembers)
            {
                /*
                 * If the callPeer reports itself as a ConferenceMember, then
                 * we've already depicted it with cpc.
                 */
                if (isConferenceMemberCallPeer(conferenceMember, callPeer))
                    continue;
                /*
                 * If the callPeer reports the local peer/user as a
                 * ConferenceMember, then we've already depicted it.
                 */
                if (isConferenceMemberLocalUser(conferenceMember))
                    continue;

                boolean addNewConferenceParticipantContainer = true;

                for (ConferenceParticipantContainer cmc : cmcs)
                {
                    if (cmc.getParticipant() == conferenceMember)
                    {
                        /*
                         * It is possible to have a ConferenceMember who is
                         * sending video but we just do not have the SSRC of
                         * that video to associate the video with the
                         * ConferenceMember. In such a case, we may be depicting
                         * the ConferenceMember twice: once with video without a
                         * ConferenceMember and once with a ConferenceMember
                         * without video. This will surely be the case at the
                         * time of this writing with non-focus participants in a
                         * telephony conference hosted on a Jitsi Videobridge.
                         * Such a display is undesirable. If the
                         * conferenceMember is known to send video, we will not
                         * display it until we associated it with a video. This
                         * way, if a ConferenceMember is not sending video, we
                         * will depict it and we can be sure that no video
                         * without a ConferenceMember association will be
                         * depicting it a second time.
                         */
                        if (cmc.toBeRemoved
                                && !conferenceMember
                                        .getVideoStatus()
                                            .allowsSending())
                        {
                            cmc.setVideo(null);
                            cmc.toBeRemoved = false;
                        }
                        addNewConferenceParticipantContainer = false;
                        break;
                    }
                }

                if (addNewConferenceParticipantContainer)
                {
                    ConferenceParticipantContainer cmc
                        = new ConferenceParticipantContainer(
                                conferenceMember,
                                null);

                    cmcs.add(cmc);
                }
            }
        }

        if ((cmcs != null) && !cmcs.isEmpty())
        {
            removeConferenceMemberContainers(cpc, false);
            /*
             * If cpc is already added to the user interface hierarchy of this
             * instance, then it was there before the update procedure and it
             * was determined to be appropriate to continue to depict its model.
             * Consequently, its Component will be neither added to (because it
             * was already added) nor removed from the user interface hierarchy
             * of this instance. That's why we have make sure that the
             * Components of its conferenceMemberContainers are also added to
             * the user interface.
             */
            if (UIVideoHandler2.isAncestor(this, cpc.getComponent()))
                addConferenceMemberContainers(cpc);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected ConferenceCallPeerRenderer updateViewFromModel(
            ConferenceCallPeerRenderer callPeerPanel,
            CallPeer callPeer)
    {
        if (callPeer == null)
        {
            /*
             * The local peer/user will be represented by a Call which has a
             * CallPeer who provides local video. However, if the user has
             * selected to hide the local video, the local peer/user will not be
             * represented at all.
             */
            Component video = null;

            if (uiVideoHandler.isLocalVideoVisible())
            {
                for (Call aCall : callConference.getCalls())
                {
                    Iterator<? extends CallPeer> callPeerIter
                        = aCall.getCallPeers();
                    OperationSetVideoTelephony videoTelephony
                        = aCall.getProtocolProvider().getOperationSet(
                                OperationSetVideoTelephony.class);

                    while (callPeerIter.hasNext())
                    {
                        callPeer = callPeerIter.next();

                        if (videoTelephony != null)
                        {
                            try
                            {
                                video
                                    = videoTelephony.getLocalVisualComponent(
                                            callPeer);
                            }
                            catch (OperationFailedException ofe)
                            {
                                logger.error(
                                        "Failed to retrieve the local video"
                                            + " for display",
                                        ofe);
                            }
                            if (video != null)
                                break;
                        }
                    }
                    if (video != null)
                        break;
                }
            }

            if (callPeer == null)
                callPeerPanel = null;
            else
            {
                Call call = callPeer.getCall();

                if (callPeerPanel instanceof ConferenceParticipantContainer)
                {
                    ConferenceParticipantContainer cpc
                        = (ConferenceParticipantContainer) callPeerPanel;

                    if (cpc.getParticipant() == call)
                        cpc.setVideo(video);
                    else
                        callPeerPanel = null;
                }
                else
                    callPeerPanel = null;
                if (callPeerPanel == null)
                {
                    callPeerPanel
                        = new ConferenceParticipantContainer(call, video);
                }
            }
        }
        else
        {
            /*
             * The specified callPeer will be represented by one of its remote
             * videos which is not associated with a ConferenceMember or is
             * associated with a ConferenceMember representing the callPeer
             * itself.
             */
            OperationSetVideoTelephony videoTelephony
                = callPeer.getProtocolProvider().getOperationSet(
                        OperationSetVideoTelephony.class);
            List<Component> videos = null;
            Component video = null;

            if (videoTelephony != null)
            {
                videos = videoTelephony.getVisualComponents(callPeer);
                if ((videos != null) && !videos.isEmpty())
                {
                    for (Component aVideo : videos)
                    {
                        ConferenceMember conferenceMember
                            = videoTelephony.getConferenceMember(
                                    callPeer,
                                    aVideo);

                        if (isConferenceMemberCallPeer(
                                conferenceMember,
                                callPeer))
                        {
                            video = aVideo;
                            break;
                        }
                    }
                }
            }

            ConferenceParticipantContainer cpc = null;

            if (callPeerPanel instanceof ConferenceParticipantContainer)
            {
                cpc = (ConferenceParticipantContainer) callPeerPanel;
                if (cpc.getParticipant() == callPeer)
                    cpc.setVideo(video);
                else
                    cpc = null;
            }
            if (cpc == null)
                cpc = new ConferenceParticipantContainer(callPeer, video);
            callPeerPanel = cpc;

            // Update the conferenceMemberContainers of the cpc.
            updateConferenceMemberContainers(cpc, videos, videoTelephony);
        }
        return callPeerPanel;
    }

    /**
     * {@inheritDoc}
     *
     * If {@link #SHOW_TOOLBARS} is <tt>false</tt>, disables the use of
     * <tt>ConferenceParticipantContainer</tt>. A reason for such a value of
     * <tt>SHOW_TOOLBARS</tt> may be that the functionality implemented in the
     * model may not fully support mapping of visual <tt>Component</tt>s
     * displaying video to telephony conference participants (e.g. in telephony
     * conferences utilizing the Jitsi Videobridge server-side technology). In
     * such a case displays the videos only, does not map videos to participants
     * and does not display participants who do not have videos.
     */
    @Override
    protected void updateViewFromModelInEventDispatchThread()
    {
        if (SHOW_TOOLBARS)
        {
            super.updateViewFromModelInEventDispatchThread();
            return;
        }

        /*
         * Determine the set of visual Components displaying video streaming
         * between the local peer/user and the remote peers which are to be
         * depicted by this instance.
         */
        Component localVideo = null;
        Set<Component> videos = new HashSet<Component>();

        for (Call call : callConference.getCalls())
        {
            OperationSetVideoTelephony videoTelephony
                = call.getProtocolProvider().getOperationSet(
                        OperationSetVideoTelephony.class);

            if (videoTelephony == null)
                continue;

            Iterator<? extends CallPeer> callPeerIter = call.getCallPeers();

            while (callPeerIter.hasNext())
            {
                CallPeer callPeer = callPeerIter.next();

                /*
                 * TODO VideoConferenceCallPanel respects
                 * UIVideoHandler2.isLocalVideoVisible() in order to react to
                 * the associated button at the bottom of the CallPanel.
                 * However, it does not add a close button on top of the local
                 * video in contrast to OneToOneCallPeerPanel. Overall, the
                 * result is questionable.
                 */
                if (uiVideoHandler.isLocalVideoVisible()
                        && (localVideo == null))
                {
                    try
                    {
                        localVideo
                            = videoTelephony.getLocalVisualComponent(callPeer);
                    }
                    catch (OperationFailedException ofe)
                    {
                        /*
                         * We'll just try to get the local video through another
                         * CallPeer then.
                         */
                    }
                    if (localVideo != null)
                        videos.add(localVideo);
                }

                List<Component> callPeerRemoteVideos
                    = videoTelephony.getVisualComponents(callPeer);

                videos.addAll(callPeerRemoteVideos);
            }
        }

        /*
         * Remove the Components of this view which are no longer present in the
         * model.
         */
        Iterator<Component> thisVideoIter = this.videos.iterator();

        while (thisVideoIter.hasNext())
        {
            Component thisVideo = thisVideoIter.next();

            if (!videos.contains(thisVideo))
            {
                thisVideoIter.remove();
                videoContainer.remove(thisVideo);
            }

            /*
             * If a video is known to be depicted by this view and is still
             * present in the model, then we could remove it from the set of
             * videos present in the model in order to prevent going through the
             * procedure of adding it to this view. However, we choose to play
             * on the safe side.
             */
        }

        /*
         * Add the Components of the model which are not depicted by this view.
         */
        for (Component video : videos)
        {
            if (!UIVideoHandler2.isAncestor(videoContainer, video))
            {
                this.videos.add(video);
                videoContainer.add(
                        video,
                        (video == localVideo)
                            ? VideoLayout.LOCAL
                            : VideoLayout.CENTER_REMOTE);
            }
        }
    }

    @Override
    protected void viewForModelAdded(
            ConferenceCallPeerRenderer callPeerPanel,
            CallPeer callPeer)
    {
        videoContainer.add(
                callPeerPanel.getComponent(),
                VideoLayout.CENTER_REMOTE);
        if ((callPeer != null)
                && (callPeerPanel instanceof ConferenceParticipantContainer))
        {
            addConferenceMemberContainers(
                    (ConferenceParticipantContainer) callPeerPanel);
        }
    }

    @Override
    protected void viewForModelRemoved(
            ConferenceCallPeerRenderer callPeerPanel,
            CallPeer callPeer)
    {
        videoContainer.remove(callPeerPanel.getComponent());
        if ((callPeer != null)
                && (callPeerPanel instanceof ConferenceParticipantContainer))
        {
            removeConferenceMemberContainers(
                    (ConferenceParticipantContainer) callPeerPanel,
                    true);
        }
    }

    /**
     * Implements an AWT <tt>Component</tt> which contains the user interface
     * elements depicting a specific participant in the telephony conference
     * depicted by a <tt>VideoConferenceCallPanel</tt>.
     */
    private class ConferenceParticipantContainer
        extends TransparentPanel
        implements ConferenceCallPeerRenderer
    {
        /**
         * The list of <tt>ConferenceParticipantContainer</tt>s which represent
         * the <tt>ConferenceMember</tt>s of the participant represented by this
         * instance. Since a <tt>CallPeer</tt> may send the local peer/user
         * multiple videos without providing a way to associate a
         * ConferenceMember with each one of them, the list may contain
         * <tt>ConferenceParticipantContainer</tt>s which do not represent a
         * specific <tt>ConferenceMember</tt> instance but rather a video sent
         * by a <tt>CallPeer</tt> to the local peer/user which looks like (in
         * the terms of <tt>VideoConferenceCallPanel) a member of a conference
         * organized by the <tt>CallPeer</tt> in question.
         * <p>
         * Implements a state which is private to
         * <tt>VideoConferenceCallPanel</tt> and is of no concern to
         * <tt>ConferenceParticipantContainer</tt>.
         * </p>
         */
        List<ConferenceParticipantContainer> conferenceMemberContainers;

        /**
         * The indicator which determines whether this instance is to be removed
         * because it has become out-of-date, obsolete, unnecessary.
         * <p>
         * Implements a state which is private to
         * <tt>VideoConferenceCallPanel</tt> and is of no concern to
         * <tt>ConferenceParticipantContainer</tt>.
         * </p>
         */
        boolean toBeRemoved;

        /**
         * The <tt>BasicConferenceParticipantPanel</tt> which is displayed at
         * the bottom of this instance, bellow the {@link #video} (i.e.
         * {@link #videoContainer}) and is referred to as the tool bar.
         */
        private final BasicConferenceParticipantPanel<?> toolBar;

        /**
         * The visual <tt>Component</tt>, if any, displaying video which is
         * depicted by this instance.
         */
        private Component video;

        /**
         * The <tt>VideoContainer</tt> which lays out the video depicted by this
         * instance i.e. {@link #video}.
         */
        private final VideoContainer videoContainer;

        /**
         * The <tt>CallPeer</tt> associated with this container, if it has been
         * created to represent a <tt>CallPeer</tt>.
         */
        private CallPeer callPeer;

        /**
         * The <tt>conferenceMember</tt> associated with this container, if it
         * has been created to represent a <tt>conferenceMember</tt>.
         */
        private ConferenceMember conferenceMember;

        /**
         * Indicates that this container contains information for the local
         * user.
         */
        private boolean isLocalUser;

        /**
         * Initializes a new <tt>ConferenceParticipantContainer</tt> instance
         * which is to depict the local peer/user.
         *
         * @param call a <tt>Call</tt> which is to provide information about the
         * local peer/user
         * @param video the visual <tt>Component</tt>, if any, displaying the
         * video streaming from the local peer/user to the remote peer(s)
         */
        public ConferenceParticipantContainer(Call call, Component video)
        {
            this(
                    createDefaultPhotoPanel(call),
                    video,
                    new ConferencePeerPanel(
                            VideoConferenceCallPanel.this,
                            call,
                            true),
                    null, null, true);
        }

        public ConferenceParticipantContainer(
                CallPeer callPeer,
                Component video)
        {
            this(   createDefaultPhotoPanel(callPeer),
                    video,
                    new ConferencePeerPanel(
                            VideoConferenceCallPanel.this,
                            callPeer,
                            true),
                    callPeer, null, false);
        }

        private ConferenceParticipantContainer(
                Component noVideo,
                Component video,
                BasicConferenceParticipantPanel<?> toolBar,
                CallPeer callPeer,
                ConferenceMember conferenceMember,
                boolean isLocalUser)
        {
            super(new BorderLayout());

            this.callPeer = callPeer;
            this.conferenceMember = conferenceMember;
            this.isLocalUser = isLocalUser;

            videoContainer = new VideoContainer(noVideo, false);
            add(videoContainer, BorderLayout.CENTER);

            this.toolBar = toolBar;
            if (this.toolBar != null)
                add(this.toolBar, BorderLayout.SOUTH);

            if (video != null)
            {
                setVideo(video);
            }
            else
                setVisible(false);
        }

        public ConferenceParticipantContainer(
                ConferenceMember conferenceMember,
                Component video)
        {
            this(
                    createDefaultPhotoPanel(conferenceMember),
                    video,
                    new ConferenceMemberPanel(
                            VideoConferenceCallPanel.this,
                            conferenceMember,
                            true),
                    null, conferenceMember, false);
        }

        public void dispose()
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            if (delegate != null)
                delegate.dispose();

            // Dispose of the conferenceMemberContainers if any.
            /*
             * XXX The field conferenceMemberContainers implements a state
             * private to VideoConferenceCallPanel which the latter makes sure
             * to access on the AWT event dispatching thread only. Since we are
             * going out of our way here to help VideoConferenceCallPanel,
             * ensure that the mentioned synchronization rule is not violated.
             */
            CallManager.assertIsEventDispatchingThread();
            if (conferenceMemberContainers != null)
            {
                for (ConferenceParticipantContainer cmc
                        : conferenceMemberContainers)
                {
                    cmc.dispose();
                }
            }
        }

        public CallPanel getCallPanel()
        {
            return getCallRenderer().getCallContainer();
        }

        public SwingCallRenderer getCallRenderer()
        {
            return VideoConferenceCallPanel.this;
        }

        public Component getComponent()
        {
            return this;
        }

        private ConferenceCallPeerRenderer
            getConferenceCallPeerRendererDelegate()
        {
            return
                (toolBar instanceof ConferenceCallPeerRenderer)
                    ? (ConferenceCallPeerRenderer) toolBar
                    : null;
        }

        /**
         * Gets the conference participant depicted by this instance.
         *
         * @return the conference participant depicted by this instance
         */
        public Object getParticipant()
        {
            return (toolBar == null) ? null : toolBar.getParticipant();
        }

        public Component getVideo()
        {
            return video;
        }

        /**
         * {@inheritDoc}
         *
         * Delegates to the <tt>toolBar</tt>, if the latter implements
         * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
         * container only. Otherwise, returns <tt>false</tt>.
         */
        public boolean isLocalVideoVisible()
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            return (delegate == null) ? false : delegate.isLocalVideoVisible();
        }

        /**
         * {@inheritDoc}
         *
         * Delegates to the <tt>toolBar</tt>, if the latter implements
         * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
         * container only.
         */
        public void printDTMFTone(char dtmfChar)
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            if (delegate != null)
                delegate.printDTMFTone(dtmfChar);
        }

        /**
         * {@inheritDoc}
         *
         * Delegates to the <tt>toolBar</tt>, if the latter implements
         * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
         * container only.
         */
        public void securityNegotiationStarted(
                CallPeerSecurityNegotiationStartedEvent ev)
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            if (delegate != null)
                delegate.securityNegotiationStarted(ev);
        }

        /**
         * {@inheritDoc}
         *
         * Delegates to the <tt>toolBar</tt>, if the latter implements
         * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
         * container only.
         */
        public void securityOff(CallPeerSecurityOffEvent ev)
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            if (delegate != null)
                delegate.securityOff(ev);
        }

        /**
         * {@inheritDoc}
         *
         * Delegates to the <tt>toolBar</tt>, if the latter implements
         * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
         * container only.
         */
        public void securityOn(CallPeerSecurityOnEvent ev)
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            if (delegate != null)
                delegate.securityOn(ev);
        }

        /**
         * {@inheritDoc}
         *
         * Delegates to the <tt>toolBar</tt>, if the latter implements
         * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
         * container only.
         */
        public void securityPending()
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            if (delegate != null)
                delegate.securityPending();
        }

        /**
         * {@inheritDoc}
         *
         * Delegates to the <tt>toolBar</tt>, if the latter implements
         * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
         * container only.
         */
        public void securityTimeout(CallPeerSecurityTimeoutEvent ev)
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            if (delegate != null)
                delegate.securityTimeout(ev);
        }

        /**
         * {@inheritDoc}
         *
         * Delegates to the <tt>toolBar</tt>, if the latter implements
         * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
         * container only.
         */
        public void setErrorReason(String reason)
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            if (delegate != null)
                delegate.setErrorReason(reason);
        }

        /**
         * {@inheritDoc}
         *
         * Delegates to the <tt>toolBar</tt>, if the latter implements
         * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
         * container only.
         */
        public void setLocalVideoVisible(boolean visible)
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            if (delegate != null)
                delegate.setLocalVideoVisible(visible);
        }

        /**
         * {@inheritDoc}
         *
         * Delegates to the <tt>toolBar</tt>, if the latter implements
         * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
         * container only.
         */
        public void setMute(boolean mute)
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            if (delegate != null)
                delegate.setMute(mute);
        }

        /**
         * {@inheritDoc}
         *
         * Delegates to the <tt>toolBar</tt>, if the latter implements
         * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
         * container only.
         */
        public void setOnHold(boolean onHold)
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            if (delegate != null)
                delegate.setOnHold(onHold);
        }

        /**
         * {@inheritDoc}
         *
         * Delegates to the <tt>toolBar</tt>, if the latter implements
         * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
         * container only.
         */
        public void setPeerImage(byte[] image)
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            if (delegate != null)
                delegate.setPeerImage(image);
        }

        /**
         * {@inheritDoc}
         *
         * Delegates to the <tt>toolBar</tt>, if the latter implements
         * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
         * container only.
         */
        public void setPeerName(String name)
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            if (delegate != null)
                delegate.setPeerName(name);
        }

        /**
         * {@inheritDoc}
         *
         * Delegates to the <tt>toolBar</tt>, if the latter implements
         * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
         * container only.
         */
        public void setPeerState(
                CallPeerState oldState,
                CallPeerState newState,
                String stateString)
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            if (delegate != null)
                delegate.setPeerState(oldState, newState, stateString);
        }

        /**
         * {@inheritDoc}
         *
         * Delegates to the <tt>toolBar</tt>, if the latter implements
         * <tt>ConferenceCallPeerRenderer</tt>, because this instance is a
         * container only.
         */
        public void setSecurityPanelVisible(boolean visible)
        {
            ConferenceCallPeerRenderer delegate
                = getConferenceCallPeerRendererDelegate();

            if (delegate != null)
                delegate.setSecurityPanelVisible(visible);
        }

        /**
         * Sets the visual <tt>Component</tt> displaying the video associated
         * with the participant depicted by this instance.
         *
         * @param video the visual <tt>Component</tt> displaying video which is
         * to be associated with the participant depicted by this instance
         */
        void setVideo(Component video)
        {
            if (this.video != video)
            {
                if (this.video != null)
                    videoContainer.remove(this.video);

                this.video = video;

                if (this.video != null)
                {
                    setVisible(true);
                    videoContainer.add(this.video, VideoLayout.CENTER_REMOTE);
                }
                else
                    setVisible(false);

                // Update thumbnails container according to video status.
                if (thumbnailContainer != null)
                {
                    if (conferenceMember != null)
                        thumbnailContainer
                            .updateThumbnail(conferenceMember, (video != null));
                    else if (callPeer != null)
                        thumbnailContainer
                            .updateThumbnail(callPeer, (video != null));
                    else if (isLocalUser)
                        thumbnailContainer
                            .updateThumbnail((video != null));
                }
            }
        }
    }
}
