/*
 * 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;

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

import javax.swing.*;
import javax.swing.text.*;

import net.java.sip.communicator.impl.gui.*;
import net.java.sip.communicator.impl.gui.utils.*;
import net.java.sip.communicator.plugin.desktoputil.*;
import net.java.sip.communicator.plugin.desktoputil.TransparentPanel;
import net.java.sip.communicator.service.gui.call.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.service.protocol.media.*;
import net.java.sip.communicator.service.resources.*;
import net.java.sip.communicator.util.*;
import net.java.sip.communicator.util.call.CallPeerAdapter;
import net.java.sip.communicator.util.skin.*;

import org.jitsi.service.neomedia.*;
import org.jitsi.util.swing.*;

/**
 * The <tt>OneToOneCallPeerPanel</tt> is the panel containing data for a call
 * peer in a given call. It contains information like call peer
 * name, photo, call duration, etc.
 *
 * @author Yana Stamcheva
 * @author Lyubomir Marinov
 * @author Sebastien Vincent
 * @author Adam Netocny
 */
public class OneToOneCallPeerPanel
    extends TransparentPanel
    implements SwingCallPeerRenderer,
               PropertyChangeListener,
               Skinnable
{
    /**
     * The <tt>Logger</tt> used by the <tt>OneToOneCallPeerPanel</tt> class and
     * its instances for logging output.
     */
    private static final Logger logger
        = Logger.getLogger(OneToOneCallPeerPanel.class);

    /**
     * Serial version UID.
     */
    private static final long serialVersionUID = 0L;

    /**
     * Property to disable showing the dummy picture for the peer when no
     * streams present.
     */
    public static final String HIDE_PLACEHOLDER_PIC_PROP
        = "net.java.sip.communicator.impl.gui.main.call.HIDE_PLACEHOLDER_PIC";

    /**
     * The <tt>CallPeer</tt>, which is rendered in this panel.
     */
    private final CallPeer callPeer;

    /**
     * The <tt>Call</tt>, which is rendered in this panel.
     */
    private final Call call;

    /**
     * The <tt>CallPeerAdapter</tt> which implements common
     * <tt>CallPeer</tt>-related listeners on behalf of this instance.
     */
    private final CallPeerAdapter callPeerAdapter;

    /**
     * The renderer of the call.
     */
    private final SwingCallRenderer callRenderer;

    /**
     * The component showing the status of the underlying call peer.
     */
    private final JLabel callStatusLabel = new JLabel();

    /**
     * The center component.
     */
    private final VideoContainer center;

    /**
     * The AWT <tt>Component</tt> which implements a button which allows
     * closing/hiding the visual <tt>Component</tt> which depicts the video
     * streaming from the local peer/user to the remote peer(s).
     */
    private Component closeLocalVisualComponentButton;

    /**
     * A listener to desktop sharing granted/revoked events and to mouse and
     * keyboard interaction with the remote video displaying the remote desktop.
     */
    private final DesktopSharingMouseAndKeyboardListener
        desktopSharingMouseAndKeyboardListener;

    /**
     * The indicator which determines whether {@link #dispose()} has already
     * been invoked on this instance. If <tt>true</tt>, this instance is
     * considered non-functional and is to be left to the garbage collector.
     */
    private boolean disposed = false;

    /**
     * The DTMF label.
     */
    private final JLabel dtmfLabel = new JLabel();

    /**
     * The component responsible for displaying an error message.
     */
    private JTextComponent errorMessageComponent;

    /**
     * The label showing whether the call is on or off hold.
     */
    private final JLabel holdStatusLabel = new JLabel();

    /**
     * Sound local level label.
     */
    private InputVolumeControlButton localLevel;

    /**
     * The <tt>Component</tt> which
     * {@link #updateViewFromModelInEventDispatchThread()} last added to
     * {@link #center} as the visual <tt>Component</tt> displaying the video
     * streaming from the local peer/user to the remote peer(s).
     * <p>
     * <b>Warning</b>: It is not to be used for any other purposes because it
     * may not represent the current state of the model of this view.
     * </p>
     */
    private Component localVideo;

    /**
     * The label showing whether the voice has been set to mute.
     */
    private final JLabel muteStatusLabel = new JLabel();

    /**
     * The <tt>Icon</tt> which represents the avatar of the associated call
     * peer.
     */
    private ImageIcon peerImage;

    /**
     * The name of the peer.
     */
    private String peerName;

    /**
     * The label containing the user photo.
     */
    private final JLabel photoLabel;

    /**
     * The listener that will listen when retrieving contact details.
     */
    private DisplayNameAndImageChangeListener detailsChangeListener = null;

    /**
     * Sound remote level label.
     */
    private Component remoteLevel;

    /**
     * The <tt>Component</tt> which
     * {@link #updateViewFromModelInEventDispatchThread()} last added to
     * {@link #center} as the visual <tt>Component</tt> displaying the video
     * streaming from the remote peer(s) to the local peer/user.
     * <p>
     * <b>Warning</b>: It is not to be used for any other purposes because it
     * may not represent the current state of the model of this view.
     * </p>
     */
    private Component remoteVideo;

    /**
     * Current id for security image.
     */
    private ImageID securityImageID = ImageLoader.SECURE_BUTTON_OFF;

    /**
     * The panel containing security related components.
     */
    private SecurityPanel<?> securityPanel;

    /**
     * The security status of the peer
     */
    private final SecurityStatusLabel securityStatusLabel
        = new SecurityStatusLabel();

    /**
     * The status bar component.
     */
    private final Component statusBar;

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

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

    /**
     * The <tt>Runnable</tt> which is scheduled by
     * {@link #updateViewFromModel()} for execution in the AWT event dispatching
     * thread in order to invoke
     * {@link #updateViewFromModelInEventDispatchThread()}.
     */
    private final Runnable updateViewFromModelInEventDispatchThread
        = new Runnable()
        {
            public void run()
            {
                /*
                 * We receive events/notifications from various threads and we
                 * respond to them in the AWT event dispatching thread. It is
                 * possible to first schedule an event to be brought to the AWT
                 * event dispatching thread, then to have #dispose() invoked on
                 * this instance and, finally, to receive the scheduled event in
                 * the AWT event dispatching thread. In such a case, this
                 * disposed instance should not respond to the event.
                 */
                if (!disposed)
                    updateViewFromModelInEventDispatchThread();
            }
        };

    /**
     * Creates a <tt>CallPeerPanel</tt> for the given call peer.
     *
     * @param callRenderer the renderer of the call
     * @param callPeer the <tt>CallPeer</tt> represented in this panel
     * @param uiVideoHandler the facility which is to aid the new instance in
     * the dealing with the video-related information
     */
    public OneToOneCallPeerPanel(
            SwingCallRenderer callRenderer,
            CallPeer callPeer,
            UIVideoHandler2 uiVideoHandler)
    {
        this.callRenderer = callRenderer;
        this.callPeer = callPeer;
        // we need to obtain call as soon as possible
        // cause if it fails too quickly we may fail to show it
        this.call = callPeer.getCall();
        this.uiVideoHandler = uiVideoHandler;

        detailsChangeListener = new DisplayNameAndImageChangeListener();
        peerName = CallManager.getPeerDisplayName(callPeer,
            detailsChangeListener);
        securityPanel = SecurityPanel.create(this, callPeer, null);

        ImageIcon icon = getPhotoLabelIcon();
        if(icon != null)
        {
            photoLabel = new JLabel(icon);
        }
        else
        {
            photoLabel = new JLabel();
        }
        center = createCenter();
        statusBar = createStatusBar();

        setPeerImage(CallManager.getPeerImage(callPeer));

        /* Lay out the main Components of the UI. */
        setLayout(new GridBagLayout());

        GridBagConstraints cnstrnts = new GridBagConstraints();

        if (center != null)
        {
            cnstrnts.fill = GridBagConstraints.BOTH;
            cnstrnts.gridx = 0;
            cnstrnts.gridy = 1;
            cnstrnts.weightx = 1;
            cnstrnts.weighty = 1;
            add(center, cnstrnts);
        }
        if (statusBar != null)
        {
            cnstrnts.fill = GridBagConstraints.NONE;
            cnstrnts.gridx = 0;
            cnstrnts.gridy = 3;
            cnstrnts.weightx = 0;
            cnstrnts.weighty = 0;
            cnstrnts.insets = new Insets(5, 0, 0, 0);
            add(statusBar, cnstrnts);
        }

        createSoundLevelIndicators();
        initSecuritySettings();

        /*
         * Add the listeners which will be notified about changes in the model
         * and which will update this view.
         */
        callPeerAdapter = new CallPeerAdapter(callPeer, this);
        uiVideoHandler.addObserver(uiVideoHandlerObserver);

        /*
         * This view adapts to whether it is displayed in full-screen or
         * windowed mode.
         */
        if (callRenderer instanceof Component)
        {
            ((Component) callRenderer).addPropertyChangeListener(
                    CallContainer.PROP_FULL_SCREEN,
                    this);
        }

        OperationSetDesktopSharingClient desktopSharingClient
            = callPeer.getProtocolProvider().getOperationSet(
                    OperationSetDesktopSharingClient.class);
        if (desktopSharingClient != null)
        {
            desktopSharingMouseAndKeyboardListener
                = new DesktopSharingMouseAndKeyboardListener(
                        callPeer,
                        desktopSharingClient);
        }
        else
            desktopSharingMouseAndKeyboardListener = null;

        updateViewFromModel();
    }

    /**
     * Set size of the {@link #photoLabel}
     */
    private void sizePhotoLabel()
    {
        if(photoLabel.getIcon() != null)
        {
            photoLabel.setPreferredSize(new Dimension(90, 90));
        }
        else
        {
            photoLabel.setPreferredSize(new Dimension(0, 0));
        }
    }

    /**
     * Creates the <tt>Component</tt> hierarchy of the central area of this
     * <tt>CallPeerPanel</tt> which displays the photo of the <tt>CallPeer</tt>
     * or the video if any.
     */
    private VideoContainer createCenter()
    {
        sizePhotoLabel();

        return createVideoContainer(photoLabel);
    }

    /**
     * Creates sound level related components.
     */
    private void createSoundLevelIndicators()
    {
        TransparentPanel localLevelPanel
            = new TransparentPanel(new BorderLayout(5, 0));
        TransparentPanel remoteLevelPanel
            = new TransparentPanel(new BorderLayout(5, 0));

        localLevel
            = new InputVolumeControlButton(
                    call.getConference(),
                    ImageLoader.MICROPHONE,
                    ImageLoader.MUTE_BUTTON,
                    false,
                    false);
        remoteLevel
            = new OutputVolumeControlButton(
                    call.getConference(),
                    ImageLoader.HEADPHONE,
                    false,
                    false)
                .getComponent();

        final SoundLevelIndicator localLevelIndicator
            = new SoundLevelIndicator(
                    SoundLevelChangeEvent.MIN_LEVEL,
                    SoundLevelChangeEvent.MAX_LEVEL);
        final SoundLevelIndicator remoteLevelIndicator
            = new SoundLevelIndicator(
                    SoundLevelChangeEvent.MIN_LEVEL,
                    SoundLevelChangeEvent.MAX_LEVEL);

        localLevelPanel.add(localLevel, BorderLayout.WEST);
        localLevelPanel.add(localLevelIndicator, BorderLayout.CENTER);
        remoteLevelPanel.add(remoteLevel, BorderLayout.WEST);
        remoteLevelPanel.add(remoteLevelIndicator, BorderLayout.CENTER);

        GridBagConstraints constraints = new GridBagConstraints();
        constraints.fill = GridBagConstraints.NONE;
        constraints.gridx = 0;
        constraints.gridy = 5;
        constraints.weightx = 0;
        constraints.weighty = 0;
        constraints.insets = new Insets(10, 0, 0, 0);

        add(localLevelPanel, constraints);

        constraints.fill = GridBagConstraints.NONE;
        constraints.gridx = 0;
        constraints.gridy = 6;
        constraints.weightx = 0;
        constraints.weighty = 0;
        constraints.insets = new Insets(5, 0, 10, 0);

        add(remoteLevelPanel, constraints);

        if(!GuiActivator.getConfigurationService().getBoolean(
                "net.java.sip.communicator.impl.gui.main.call."
                    + "DISABLE_SOUND_LEVEL_INDICATORS",
                false))
        {
            callPeer.addStreamSoundLevelListener(
                    new SoundLevelListener()
                    {
                        public void soundLevelChanged(Object source, int level)
                        {
                            remoteLevelIndicator.updateSoundLevel(level);
                        }
                    });
            /*
             * By the time the UI gets to be initialized, the callPeer may have
             * been removed from its Call. As far as the UI is concerned, the
             * callPeer will never have a Call again and there will be no audio
             * levels to display anyway so there is no point in throwing a
             * NullPointerException here.
             */
            if (call != null)
            {
                call.addLocalUserSoundLevelListener(
                        new SoundLevelListener()
                        {
                            public void soundLevelChanged(
                                    Object source,
                                    int level)
                            {
                                localLevelIndicator.updateSoundLevel(level);
                            }
                        });
            }
        }
    }

    /**
     * Creates the <tt>Component</tt> hierarchy of the area of
     * status-related information such as <tt>CallPeer</tt> display
     * name, call duration, security status.
     *
     * @return the root of the <tt>Component</tt> hierarchy of the area of
     *         status-related information such as <tt>CallPeer</tt>
     *         display name, call duration, security status
     */
    private Component createStatusBar()
    {
        // stateLabel
        callStatusLabel.setForeground(Color.WHITE);
        dtmfLabel.setForeground(Color.WHITE);
        callStatusLabel.setText(callPeer.getState().getLocalizedStateString());

        PeerStatusPanel statusPanel = new PeerStatusPanel(new GridBagLayout());

        GridBagConstraints constraints = new GridBagConstraints();

        constraints.gridx = 0;
        constraints.gridy = 0;
        statusPanel.add(securityStatusLabel, constraints);
        initSecurityStatusLabel();

        constraints.gridx++;
        statusPanel.add(holdStatusLabel, constraints);

        constraints.gridx++;
        statusPanel.add(muteStatusLabel, constraints);

        constraints.gridx++;
        callStatusLabel.setBorder(
            BorderFactory.createEmptyBorder(2, 3, 2, 12));
        statusPanel.add(callStatusLabel, constraints);

        constraints.gridx++;
        constraints.weightx = 1f;
        statusPanel.add(dtmfLabel, constraints);

        return statusPanel;
    }

    /**
     * Creates a new AWT <tt>Container</tt> which can display a single
     * <tt>Component</tt> at a time (supposedly, one which represents video)
     * and, in the absence of such a <tt>Component</tt>, displays a
     * predefined default <tt>Component</tt> (in accord with the previous
     * supposition, one which is the default when there is no video). The
     * returned <tt>Container</tt> will track the <tt>Components</tt>s
     * added to and removed from it in order to make sure that
     * <tt>noVideoContainer</tt> is displayed as described.
     *
     * @param noVideoComponent the predefined default <tt>Component</tt> to
     *            be displayed in the returned <tt>Container</tt> when there
     *            is no other <tt>Component</tt> in it
     * @return a new <tt>Container</tt> which can display a single
     *         <tt>Component</tt> at a time and, in the absence of such a
     *         <tt>Component</tt>, displays <tt>noVideoComponent</tt>
     */
    private VideoContainer createVideoContainer(Component noVideoComponent)
    {
        Container oldParent = noVideoComponent.getParent();

        if (oldParent != null)
            oldParent.remove(noVideoComponent);

        return new VideoContainer(noVideoComponent, false);
    }

    /**
     * Releases the resources acquired by this instance which require explicit
     * disposal (e.g. any listeners added to the depicted <tt>CallPeer</tt>.
     * Invoked by <tt>OneToOneCallPanel</tt> when it determines that this
     * <tt>OneToOneCallPeerPanel</tt> is no longer necessary.
     */
    public void dispose()
    {
        disposed = true;

        if(detailsChangeListener != null)
            detailsChangeListener.setInterested(false);

        callPeerAdapter.dispose();
        uiVideoHandler.deleteObserver(uiVideoHandlerObserver);

        if (callRenderer instanceof Component)
        {
            ((Component) callRenderer).removePropertyChangeListener(
                    CallContainer.PROP_FULL_SCREEN,
                    this);
        }
    }

    /**
     * Returns the parent <tt>CallPanel</tt> containing this renderer.
     * @return the parent <tt>CallPanel</tt> containing this renderer
     */
    public CallPanel getCallPanel()
    {
        return callRenderer.getCallContainer();
    }

    /**
     * Returns the parent call renderer.
     *
     * @return the parent call renderer
     */
    public CallRenderer getCallRenderer()
    {
        return callRenderer;
    }

    /**
     * Returns the component associated with this renderer.
     *
     * @return the component associated with this renderer
     */
    public Component getComponent()
    {
        return this;
    }

    /**
     * Returns the name of the peer, contained in this panel.
     *
     * @return the name of the peer, contained in this panel
     */
    public String getPeerName()
    {
        return peerName;
    }

    /**
     * Tests a provided boolean property name, returning false if it should be
     * hidden.
     *
     * @param componentHidePropertyName the name of the boolean property to
     *        check.
     * @return false if the component should be hidden, true otherwise.
     *
     */
    private boolean isComponentEnabled(String componentHidePropertyName)
    {
        return !GuiActivator.getConfigurationService().getBoolean(
            componentHidePropertyName,
            false);
    }

    /**
     * Gets the <tt>Icon</tt> to be displayed in {@link #photoLabel}.
     *
     * @return the <tt>Icon</tt> to be displayed in {@link #photoLabel}
     */
    private ImageIcon getPhotoLabelIcon()
    {
        return
            (peerImage == null && isComponentEnabled(HIDE_PLACEHOLDER_PIC_PROP))
                ? new ImageIcon(
                        ImageLoader.getImage(ImageLoader.DEFAULT_USER_PHOTO))
                : peerImage;
    }

    /**
     * Initializes the security settings for this call peer.
     */
    private void initSecuritySettings()
    {
        CallPeerSecurityStatusEvent securityEvent
            = callPeer.getCurrentSecuritySettings();

        if (securityEvent instanceof CallPeerSecurityOnEvent)
            securityOn((CallPeerSecurityOnEvent) securityEvent);
    }

    /**
     * Initializes the security status label, shown in the call status bar.
     */
    private void initSecurityStatusLabel()
    {
        securityStatusLabel.setBorder(
            BorderFactory.createEmptyBorder(2, 5, 2, 5));

        securityStatusLabel.addMouseListener(new MouseAdapter()
        {
            /**
             * Invoked when a mouse button has been pressed on a component.
             */
            @Override
            public void mousePressed(MouseEvent e)
            {
                // Only show the security details if the security is on.
                SrtpControl ctrl = securityPanel.getSecurityControl();
                if (ctrl instanceof ZrtpControl
                    && ctrl.getSecureCommunicationStatus())
                {
                    setSecurityPanelVisible(!callRenderer.getCallContainer()
                        .getCallWindow().getFrame().getGlassPane().isVisible());
                }
            }
        });
    }

    /**
     * Determines whether the visual <tt>Component</tt> depicting the video
     * streaming from the local peer/user to the remote peer(s) is currently
     * visible.
     *
     * @return <tt>true</tt> if the visual <tt>Component</tt> depicting the
     * video streaming from the local peer/user to the remote peer(s) is
     * currently visible; otherwise, <tt>false</tt>
     */
    public boolean isLocalVideoVisible()
    {
        return uiVideoHandler.isLocalVideoVisible();
    }

    /**
     * Reloads all icons.
     */
    public void loadSkin()
    {
        if(localLevel != null)
            localLevel.setIcon(new ImageIcon(
                ImageLoader.getImage(ImageLoader.MICROPHONE)));

        if(remoteLevel != null && remoteLevel instanceof Skinnable)
            ((Skinnable) remoteLevel).loadSkin();

        if(muteStatusLabel.getIcon() != null)
            muteStatusLabel.setIcon(new ImageIcon(
                ImageLoader.getImage(ImageLoader.MUTE_STATUS_ICON)));

        if(holdStatusLabel.getIcon() != null)
            holdStatusLabel.setIcon(new ImageIcon(
                ImageLoader.getImage(ImageLoader.HOLD_STATUS_ICON)));

        securityStatusLabel.setIcon(new ImageIcon(
                ImageLoader.getImage(securityImageID)));

        if(peerImage == null)
        {
            if(isComponentEnabled(HIDE_PLACEHOLDER_PIC_PROP))
            {
                photoLabel.setIcon(
                        new ImageIcon(
                                ImageLoader.getImage(
                                        ImageLoader.DEFAULT_USER_PHOTO)));
            }
            else
            {
                photoLabel.setIcon(null);
            }
            sizePhotoLabel();
        }
    }

    /**
     * Prints the given DTMG character through this <tt>CallPeerRenderer</tt>.
     * @param dtmfChar the DTMF char to print
     */
    public void printDTMFTone(char dtmfChar)
    {
        dtmfLabel.setText(dtmfLabel.getText() + dtmfChar);
        if (dtmfLabel.getBorder() == null)
            dtmfLabel.setBorder(
                BorderFactory.createEmptyBorder(2, 1, 2, 5));
    }

    /**
     * Notifies this instance about a change in the value of a property of a
     * source which of interest to this instance. For example,
     * <tt>OneToOneCallPeerPanel</tt> updates its user interface-related
     * properties upon changes in the value of the
     * {@link CallContainer#PROP_FULL_SCREEN} property of its associated
     * {@link #callRenderer}.
     *
     * @param ev a <tt>PropertyChangeEvent</tt> which identifies the source, the
     * name of the property and the old and new values
     */
    public void propertyChange(PropertyChangeEvent ev)
    {
        if (CallContainer.PROP_FULL_SCREEN.equals(ev.getPropertyName()))
            updateViewFromModel();
    }

    /**
     * Re-dispatches glass pane mouse events only in case they occur on the
     * security panel.
     *
     * @param glassPane the glass pane
     * @param e the mouse event in question
     */
    private void redispatchMouseEvent(Component glassPane, MouseEvent e)
    {
        Point glassPanePoint = e.getPoint();

        Point securityPanelPoint = SwingUtilities.convertPoint(
                                        glassPane,
                                        glassPanePoint,
                                        securityPanel);

        Component component;
        Point componentPoint;

        if (securityPanelPoint.y > 0)
        {
            component = securityPanel;
            componentPoint = securityPanelPoint;
        }
        else
        {
            Container contentPane
                = callRenderer.getCallContainer().getCallWindow()
                                    .getFrame().getContentPane();

            Point containerPoint = SwingUtilities.convertPoint(
                                                    glassPane,
                                                    glassPanePoint,
                                                    contentPane);

            component = SwingUtilities.getDeepestComponentAt(contentPane,
                    containerPoint.x, containerPoint.y);

            componentPoint = SwingUtilities.convertPoint(contentPane,
                glassPanePoint, component);
        }

        if (component != null)
            component.dispatchEvent(new MouseEvent( component,
                                                    e.getID(),
                                                    e.getWhen(),
                                                    e.getModifiers(),
                                                    componentPoint.x,
                                                    componentPoint.y,
                                                    e.getClickCount(),
                                                    e.isPopupTrigger()));

        e.consume();
    }

    /**
     * The handler for the security event received. The security event
     * for starting establish a secure connection.
     *
     * @param evt the security started event received
     */
    public void securityNegotiationStarted(
        CallPeerSecurityNegotiationStartedEvent evt)
    {
        if(Boolean.parseBoolean(GuiActivator.getResources()
                .getSettingsString("impl.gui.PARANOIA_UI")))
        {
            SrtpControl srtpControl = null;
            if (callPeer instanceof MediaAwareCallPeer)
                srtpControl = evt.getSecurityController();

            securityPanel
                = new ParanoiaTimerSecurityPanel<SrtpControl>(srtpControl);

            setSecurityPanelVisible(true);
        }
    }

    /**
     * Indicates that the security has gone off.
     *
     * @param evt the <tt>CallPeerSecurityOffEvent</tt> that notified us
     */
    public void securityOff(final CallPeerSecurityOffEvent evt)
    {
        if(!SwingUtilities.isEventDispatchThread())
        {
            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    securityOff(evt);
                }
            });
            return;
        }

        if (evt.getSessionType() == CallPeerSecurityOffEvent.AUDIO_SESSION)
        {
            securityStatusLabel.setText("");
            securityStatusLabel.setSecurityOff();
            if (securityStatusLabel.getBorder() == null)
                securityStatusLabel.setBorder(
                    BorderFactory.createEmptyBorder(2, 5, 2, 3));
        }

        securityPanel.securityOff(evt);
    }

    /**
     * Indicates that the security is turned on.
     * <p>
     * Sets the secured status icon to the status panel and initializes/updates
     * the corresponding security details.
     * @param evt Details about the event that caused this message.
     */
    public void securityOn(final CallPeerSecurityOnEvent evt)
    {
        if(!SwingUtilities.isEventDispatchThread())
        {
            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    securityOn(evt);
                }
            });
            return;
        }

        // If the securityOn is called without a specific event, we'll just set
        // the security label status to on.
        if (evt == null)
        {
            securityStatusLabel.setSecurityOn();
            return;
        }

        SrtpControl srtpControl = evt.getSecurityController();

        if (!srtpControl.requiresSecureSignalingTransport()
                || callPeer.getProtocolProvider().isSignalingTransportSecure())
        {
            if (srtpControl instanceof ZrtpControl)
            {
                securityStatusLabel.setText("zrtp");

                if (!((ZrtpControl) srtpControl).isSecurityVerified())
                    securityStatusLabel.setSecurityPending();
                else
                    securityStatusLabel.setSecurityOn();
            }
            else
                securityStatusLabel.setSecurityOn();
        }

        // if we have some other panel, using other control
        if(!srtpControl.getClass().isInstance(
                securityPanel.getSecurityControl())
            || (securityPanel instanceof ParanoiaTimerSecurityPanel))
        {
            setSecurityPanelVisible(false);

            securityPanel
                = SecurityPanel.create(this, callPeer, srtpControl);

            if (srtpControl instanceof ZrtpControl)
                ((ZrtpSecurityPanel) securityPanel)
                    .setSecurityStatusLabel(securityStatusLabel);
        }

        securityPanel.securityOn(evt);

        boolean isSecurityLowPriority = Boolean.parseBoolean(
            GuiActivator.getResources().getSettingsString(
                "impl.gui.I_DONT_CARE_THAT_MUCH_ABOUT_SECURITY"));

        // Display ZRTP panel in case SAS was not verified or a AOR mismtach
        // was detected during creation of ZrtpSecurityPanel.
        // Don't show panel if user does not care about security at all.
        if (srtpControl instanceof ZrtpControl
            && !isSecurityLowPriority
            && (!((ZrtpControl) srtpControl).isSecurityVerified()
                || ((ZrtpSecurityPanel) securityPanel).isZidAorMismatch()))
        {
            setSecurityPanelVisible(true);
        }

        this.revalidate();
    }

    /**
     * Indicates that the security status is pending confirmation.
     */
    public void securityPending()
    {
        securityStatusLabel.setSecurityPending();
    }

    /**
     * Indicates that the security is timeouted, is not supported by the
     * other end.
     * @param evt Details about the event that caused this message.
     */
    public void securityTimeout(final CallPeerSecurityTimeoutEvent evt)
    {
        if(!SwingUtilities.isEventDispatchThread())
        {
            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    securityTimeout(evt);
                }
            });
            return;
        }

        if (securityPanel != null)
            securityPanel.securityTimeout(evt);
    }

    /**
     * Sets the reason of a call failure if one occurs. The renderer should
     * display this reason to the user.
     * @param reason the reason to display
     */
    public void setErrorReason(final String reason)
    {
        if(!SwingUtilities.isEventDispatchThread())
        {
            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    setErrorReason(reason);
                }
            });
            return;
        }

        if (errorMessageComponent == null)
        {
            errorMessageComponent = new JTextPane();

            JTextPane textPane = (JTextPane) errorMessageComponent;
            textPane.setEditable(false);
            textPane.setOpaque(false);

            StyledDocument doc = textPane.getStyledDocument();

            MutableAttributeSet standard = new SimpleAttributeSet();
            StyleConstants.setAlignment(standard, StyleConstants.ALIGN_CENTER);
            StyleConstants.setFontFamily(standard,
                                        textPane.getFont().getFamily());
            StyleConstants.setFontSize(standard, 12);
            doc.setParagraphAttributes(0, 0, standard, true);

            GridBagConstraints constraints = new GridBagConstraints();
            constraints.fill = GridBagConstraints.HORIZONTAL;
            constraints.gridx = 0;
            constraints.gridy = 4;
            constraints.weightx = 1;
            constraints.weighty = 0;
            constraints.insets = new Insets(5, 0, 0, 0);

            add(errorMessageComponent, constraints);
            this.revalidate();
        }

        errorMessageComponent.setText(reason);

        if (isVisible())
            errorMessageComponent.repaint();
    }

    /**
     * Shows/hides the visual <tt>Component</tt> depicting the video streaming
     * from the local peer/user to the remote peer(s).
     *
     * @param visible <tt>true</tt> to show the visual <tt>Component</tt>
     * depicting the video streaming from the local peer/user to the remote
     * peer(s); <tt>false</tt>, otherwise
     */
    public void setLocalVideoVisible(boolean visible)
    {
        uiVideoHandler.setLocalVideoVisible(visible);
    }

    /**
     * Sets the mute status icon to the status panel.
     *
     * @param isMute indicates if the call with this peer is
     * muted
     */
    public void setMute(final boolean isMute)
    {
        if(!SwingUtilities.isEventDispatchThread())
        {
            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    setMute(isMute);
                }
            });
            return;
        }

        if(isMute)
        {
            muteStatusLabel.setIcon(new ImageIcon(
                ImageLoader.getImage(ImageLoader.MUTE_STATUS_ICON)));
            muteStatusLabel.setBorder(
                BorderFactory.createEmptyBorder(2, 3, 2, 3));
        }
        else
        {
            muteStatusLabel.setIcon(null);
            muteStatusLabel.setBorder(
                BorderFactory.createEmptyBorder(0, 0, 0, 0));
        }

        // Update input volume control button state to reflect the current
        // mute status.
        if (localLevel.isSelected() != isMute)
            localLevel.setSelected(isMute);

        this.revalidate();
        this.repaint();
    }

    /**
     * Sets the "on hold" property value.
     * @param isOnHold indicates if the call with this peer is put on hold
     */
    public void setOnHold(final boolean isOnHold)
    {
        if(!SwingUtilities.isEventDispatchThread())
        {
            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    setOnHold(isOnHold);
                }
            });
            return;
        }

        if(isOnHold)
        {
            holdStatusLabel.setIcon(new ImageIcon(
                ImageLoader.getImage(ImageLoader.HOLD_STATUS_ICON)));
            holdStatusLabel.setBorder(
                BorderFactory.createEmptyBorder(2, 3, 2, 3));
        }
        else
        {
            holdStatusLabel.setIcon(null);
            holdStatusLabel.setBorder(
                BorderFactory.createEmptyBorder(0, 0, 0, 0));
        }

        this.revalidate();
        this.repaint();
    }

    /**
     * Set the image of the peer
     *
     * @param image new image
     */
    public void setPeerImage(byte[] image)
    {
        // If the image is still null we try to obtain it from one of the
        // available contact sources.
        if (image == null || image.length <= 0)
        {
            // will do nothing, as querying for display name will also
            // set and image if it exist
        }
        else
        {
            peerImage = ImageUtils.getScaledRoundedIcon(image, 100, 100);
            if (peerImage == null)
                peerImage = getPhotoLabelIcon();

            if(!SwingUtilities.isEventDispatchThread())
            {
                SwingUtilities.invokeLater(new Runnable()
                {
                    public void run()
                    {
                        photoLabel.setIcon(peerImage);
                        sizePhotoLabel();
                        photoLabel.repaint();
                    }
                });
            }
            else
            {
                photoLabel.setIcon(peerImage);
                sizePhotoLabel();
                photoLabel.repaint();
            }
        }
    }

    /**
     * Sets the name of the peer.
     * @param name the name of the peer
     */
    public void setPeerName(final String name)
    {
        if(!SwingUtilities.isEventDispatchThread())
        {
            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    setPeerName(name);
                }
            });
            return;
        }

        peerName = name;

        ((OneToOneCallPanel) callRenderer).setPeerName(name);
    }

    /**
     * Sets the state of the contained call peer by specifying the
     * state name.
     *
     * @param oldState the previous state of the peer
     * @param newState the new state of the peer
     * @param stateString the state of the contained call peer
     */
    public void setPeerState(final CallPeerState oldState,
                             final CallPeerState newState,
                             final String stateString)
    {
        if(!SwingUtilities.isEventDispatchThread())
        {
            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    setPeerState(oldState, newState, stateString);
                }
            });
            return;
        }

        this.callStatusLabel.setText(stateString);

        if (newState == CallPeerState.CONNECTED
            && !CallPeerState.isOnHold(oldState)
            && !securityStatusLabel.isSecurityStatusSet())
        {
            securityStatusLabel.setSecurityOff();
        }
    }

    /**
     * Shows/hides the security panel.
     *
     * @param isVisible <tt>true</tt> to show the security panel, <tt>false</tt>
     * to hide it
     */
    public void setSecurityPanelVisible(final boolean isVisible)
    {
        if(!SwingUtilities.isEventDispatchThread())
        {
            SwingUtilities.invokeLater(new Runnable()
            {
                public void run()
                {
                    setSecurityPanelVisible(isVisible);
                }
            });
            return;
        }

        final JFrame callFrame = callRenderer.getCallContainer()
            .getCallWindow().getFrame();

        final JPanel glassPane = (JPanel) callFrame.getGlassPane();

        if (!isVisible)
        {
            // Need to hide the security panel explicitly in order to keep the
            // fade effect.
            securityPanel.setVisible(false);
            glassPane.setVisible(false);
            glassPane.removeAll();
        }
        else
        {
            glassPane.setLayout(null);
            glassPane.addMouseListener(new MouseListener()
            {
                public void mouseClicked(MouseEvent e)
                {
                    redispatchMouseEvent(glassPane, e);
                }

                public void mouseEntered(MouseEvent e)
                {
                    redispatchMouseEvent(glassPane, e);
                }

                public void mouseExited(MouseEvent e)
                {
                    redispatchMouseEvent(glassPane, e);
                }

                public void mousePressed(MouseEvent e)
                {
                    redispatchMouseEvent(glassPane, e);
                }

                public void mouseReleased(MouseEvent e)
                {
                    redispatchMouseEvent(glassPane, e);
                }
            });

            Point securityLabelPoint = securityStatusLabel.getLocation();

            Point newPoint
                = SwingUtilities.convertPoint(securityStatusLabel.getParent(),
                securityLabelPoint.x, securityLabelPoint.y,
                callFrame);

            securityPanel.setBeginPoint(
                new Point((int) newPoint.getX() + 15, 0));
            securityPanel.setBounds(
                0, (int) newPoint.getY() - 5, this.getWidth(), 130);

            glassPane.add(securityPanel);
            // Need to show the security panel explicitly in order to keep the
            // fade effect.
            securityPanel.setVisible(true);
            glassPane.setVisible(true);

            glassPane.addComponentListener(new ComponentAdapter()
            {
                /**
                 * Invoked when the component's size changes.
                 */
                @Override
                public void componentResized(ComponentEvent e)
                {
                    if (glassPane.isVisible())
                    {
                        glassPane.setVisible(false);
                        callFrame.removeComponentListener(this);
                    }
                }
            });
        }
    }

    /**
     * Updates this view i.e. <tt>OneToOneCallPeerPanel</tt> so that it depicts
     * the current state of its model i.e. <tt>callPeer</tt>.
     */
    private void updateViewFromModel()
    {
        /*
         * We receive events/notifications from various threads and we respond
         * to them in the AWT event dispatching thread. It is possible to first
         * schedule an event to be brought to the AWT event dispatching thread,
         * then to have #dispose() invoked on this instance and, finally, to
         * receive the scheduled event in the AWT event dispatching thread. In
         * such a case, this disposed instance should not respond to the event
         * because it may, for example, steal a visual Components depicting
         * video (which cannot belong to more than one parent at a time) from
         * another non-disposed OneToOneCallPeerPanel.
         */
        if (!disposed)
        {
            if (SwingUtilities.isEventDispatchThread())
                updateViewFromModelInEventDispatchThread();
            else
            {
                SwingUtilities.invokeLater(
                        updateViewFromModelInEventDispatchThread);
            }
        }
    }

    /**
     * Updates this view i.e. <tt>OneToOneCallPeerPanel</tt> so that it depicts
     * the current state of its model i.e. <tt>callPeer</tt>. The update is
     * performed in the AWT event dispatching thread.
     */
    private void updateViewFromModelInEventDispatchThread()
    {
        /*
         * We receive events/notifications from various threads and we respond
         * to them in the AWT event dispatching thread. It is possible to first
         * schedule an event to be brought to the AWT event dispatching thread,
         * then to have #dispose() invoked on this instance and, finally, to
         * receive the scheduled event in the AWT event dispatching thread. In
         * such a case, this disposed instance should not respond to the event
         * because it may, for example, steal a visual Components depicting
         * video (which cannot belong to more than one parent at a time) from
         * another non-disposed OneToOneCallPeerPanel.
         */
        if (disposed)
            return;

        /*
         * Update the display of visual <tt>Component</tt>s depicting video
         * streaming between the local peer/user and the remote peer(s).
         */

        OperationSetVideoTelephony videoTelephony
            = callPeer.getProtocolProvider().getOperationSet(
                    OperationSetVideoTelephony.class);
        Component remoteVideo = null;
        Component localVideo = null;

        if (videoTelephony != null)
        {
            List<Component> remoteVideos
                = videoTelephony.getVisualComponents(callPeer);

            if ((remoteVideos != null) && !remoteVideos.isEmpty())
            {
                /*
                 * TODO OneToOneCallPeerPanel displays a one-to-one conversation
                 * between the local peer/user and a specific remote peer. If
                 * the remote peer is the focus of a telephony conference of its
                 * own, it may be sending multiple videos to the local peer.
                 * Switching to a user interface which displays multiple videos
                 * is the responsibility of whoever decided that this
                 * OneToOneCallPeerPanel is to be used to depict the current
                 * state of the CallConference associated with the CallPeer
                 * depicted by this instance. If that switching decides that
                 * this instance is to continue being the user interface, then
                 * we should probably pick up the remote video which is
                 * generated by the remote peer and not one of its
                 * ConferenceMembers.
                 */
                remoteVideo = remoteVideos.get(0);
            }

            if (uiVideoHandler.isLocalVideoVisible())
            {
                try
                {
                    localVideo
                        = videoTelephony.getLocalVisualComponent(callPeer);
                }
                catch (OperationFailedException ofe)
                {
                    /*
                     * Well, we cannot do much about the exception. We'll just
                     * not display the local video.
                     */
                    logger.warn(
                            "Failed to retrieve local video to be displayed.",
                            ofe);
                }
            }

            /*
             * Determine whether there is actually a change in the local and
             * remote videos which requires an update.
             */
            boolean localVideoChanged
                = ((localVideo != this.localVideo)
                    || ((localVideo != null)
                        && !UIVideoHandler2.isAncestor(center, localVideo)));
            boolean remoteVideoChanged
                = ((remoteVideo != this.remoteVideo)
                    || ((remoteVideo != null)
                        && !UIVideoHandler2.isAncestor(center, remoteVideo)));

            // If the remote video has changed, maybe the CallPanel can display
            // the LO/SD/HD button.
            if(remoteVideoChanged)
            {
                // Updates video component which may listen the mouse and key
                // events.
                if (desktopSharingMouseAndKeyboardListener != null)
                {
                    desktopSharingMouseAndKeyboardListener.setVideoComponent(
                            remoteVideo);
                }

                CallPanel callPanel = callRenderer.getCallContainer();
                // The remote video has been added, then tries to display the
                // LO/SD/HD button.
                if(remoteVideo != null)
                {
                    callPanel.addRemoteVideoSpecificComponents(callPeer);
                }
                // The remote video has been removed, then hide the LO/SD/HD
                // button if it is currently displayed.
                else
                {
                    callPanel.removeRemoteVideoSpecificComponents();
                }
            }

            if (localVideoChanged || remoteVideoChanged)
            {
                /*
                 * VideoContainer and JAWTRenderer cannot handle random
                 * additions of Components. Removing the localVideo when the
                 * user has requests its hiding though, should work without
                 * removing all Components from the VideoCotainer and adding
                 * them again.
                 */
                if (localVideoChanged
                        && !remoteVideoChanged
                        && (localVideo == null))
                {
                    if (this.localVideo != null)
                    {
                        center.remove(this.localVideo);
                        this.localVideo = null;

                        if (closeLocalVisualComponentButton != null)
                            center.remove(closeLocalVisualComponentButton);
                    }
                }
                else
                {
                    center.removeAll();
                    this.localVideo = null;
                    this.remoteVideo = null;

                    /*
                     * AWT does not make a guarantee about the Z order even
                     * within an operating system i.e. the order of adding the
                     * Components to their Container does not mean that they
                     * will be determinedly painted in that or reverse order.
                     * Anyway, there appears to be an expectation among the
                     * developers less acquainted with AWT that AWT paints the
                     * Components of a Container in an order that is the reverse
                     * of the order of their adding. In order to satisfy that
                     * expectation and thus give at least some idea to the
                     * developers reading the code bellow, do add the Components
                     * according to that expectation.
                     */

                    if (localVideo != null)
                    {
                        if (closeLocalVisualComponentButton == null)
                        {
                            closeLocalVisualComponentButton
                                = new CloseLocalVisualComponentButton(
                                        uiVideoHandler);
                        }
                        center.add(
                                closeLocalVisualComponentButton,
                                VideoLayout.CLOSE_LOCAL_BUTTON,
                                -1);

                        center.add(localVideo, VideoLayout.LOCAL, -1);
                        this.localVideo = localVideo;
                    }

                    if (remoteVideo != null)
                    {
                        center.add(remoteVideo, VideoLayout.CENTER_REMOTE, -1);
                        this.remoteVideo = remoteVideo;
                    }
                }
            }
        }
    }

    /**
     * The <tt>TransparentPanel</tt> that will display the peer status.
     */
    private static class PeerStatusPanel
        extends TransparentPanel
    {
        /**
         * Silence the serial warning. Though there isn't a plan to serialize
         * the instances of the class, there're no fields so the default
         * serialization routine will work.
         */
        private static final long serialVersionUID = 0L;

        /**
         * Constructs a new <tt>PeerStatusPanel</tt>.
         *
         * @param layout the <tt>LayoutManager</tt> to use
         */
        public PeerStatusPanel(LayoutManager layout)
        {
            super(layout);
        }

        /**
         * @{inheritDoc}
         */
        @Override
        public void paintComponent(Graphics g)
        {
            super.paintComponent(g);

            g = g.create();

            try
            {
                AntialiasingManager.activateAntialiasing(g);

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

    /**
     * Listens for display name update and image update, some searches for
     * display name are slow, so we add a listener to update them when
     * result comes in.
     */
    private class DisplayNameAndImageChangeListener
        implements CallManager.DetailsResolveListener
    {
        /**
         * By default we are interested in events.
         */
        private boolean interested = true;

        @Override
        public void displayNameUpdated(String displayName)
        {
            setPeerName(displayName);
        }

        @Override
        public void imageUpdated(byte[] image)
        {
            setPeerImage(image);
        }

        /**
         * Are we interested.
         * @return
         */
        @Override
        public boolean isInterested()
        {
            return interested;
        }

        /**
         * Changes the interested value.
         * @param value
         */
        public void setInterested(boolean value)
        {
            this.interested = value;
        }
    }
}
