/*
 * 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.awt.event.*;
import java.util.*;

import javax.swing.*;

import net.java.sip.communicator.impl.gui.*;
import net.java.sip.communicator.impl.gui.main.call.*;
import net.java.sip.communicator.impl.gui.utils.*;
import net.java.sip.communicator.plugin.desktoputil.*;
import net.java.sip.communicator.service.globaldisplaydetails.*;
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.util.skin.*;

import net.java.sip.communicator.util.call.CallPeerAdapter;

import org.jitsi.service.neomedia.*;
import org.jitsi.service.resources.*;

/**
 * Depicts a single <tt>CallPeer</tt> who participates in a telephony conference
 * and is not a focus or the local user/peer (identified by a specific
 * <tt>Call</tt> instance).
 *
 * @author Yana Stamcheva
 * @author Lyubomir Marinov
 * @author Adam Netocny
 */
public class ConferencePeerPanel
    extends BasicConferenceParticipantPanel<Object>
    implements ConferenceCallPeerRenderer,
               Skinnable
{
    /**
     * Serial version UID.
     */
    private static final long serialVersionUID = 0L;

    /**
     * The <tt>Call</tt> which represents the local peer depicted by this
     * instance. If <tt>null</tt>, then this instance does not depict the local
     * peer and depicts {@link #callPeer}.
     */
    private final Call call;

    /**
     * The <tt>CallPeer</tt> depicted by this instance. If <tt>null</tt>, then
     * this instance depicts the local peer represented by {@link #call}.
     */
    private final CallPeer callPeer;

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

    /**
     * The tools menu available for each peer.
     */
    private CallPeerMenu callPeerMenu;

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

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

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

    /**
     * The component showing the security details.
     */
    private SecurityPanel<?> securityPanel;

    /**
     * The <tt>SoundLevelListener</tt> which listens to the changes in the
     * audio/sound level of the model of this instance. If {@link #callPeer} is
     * non-<tt>null</tt>, <tt>callPeer</tt> is the model of this instance i.e.
     * <tt>soundLevelListener</tt> will be added to the audio stream of
     * <tt>callPeer</tt> and will listen to local calculations of the audio
     * levels of the remote peer; otherwise, {@link #call} is the model of this
     * instance i.e. <tt>soundLevelListener</tt> will be added to <tt>call</tt>
     * and will listen to local calculations of the audio levels of the local
     * peer.
     */
    private final SoundLevelListenerImpl soundLevelListener
        = new SoundLevelListenerImpl();

    /**
     * Initializes a new <tt>ConferencePeerPanel</tt> which is to depict the
     * local peer represented by a specific <tt>Call</tt> on behalf of a
     * specific <tt>BasicConferenceCallPanel</tt> i.e. <tt>CallRenderer</tt>.
     *
     * @param callRenderer the <tt>BasicConferenceCallPanel</tt> which requests
     * the initialization of the new instance and which will use the new
     * instance to depict the local peer represented by the specified
     * <tt>Call</tt>
     * @param call the <tt>Call</tt> which represents the local peer to be
     * depicted by the new instance
     * @param video <tt>true</tt> if the new instance will be associated with a
     * display of video (e.g. which will be streaming to the <tt>CallPeer</tt>s
     * associated with the specified <tt>call</tt>); otherwise, <tt>false</tt>
     * @throws NullPointerException if <tt>call</tt> is <tt>null</tt>
     */
    public ConferencePeerPanel(
            BasicConferenceCallPanel callRenderer,
            Call call,
            boolean video)
    {
        super(callRenderer, call, video);

        if (call == null)
            throw new NullPointerException("call");

        this.call = call;
        this.callPeer = null;

        callPeerAdapter = null;

        String globalDisplayName = null;
        // Try to set the same image as the one in the main window. This way
        // we improve our chances to have an image, instead of looking only at
        // the protocol provider avatar, which could be null, we look for any
        // image coming from one of our accounts.
        GlobalDisplayDetailsService displayDetailsService
            = GuiActivator.getGlobalDisplayDetailsService();

        if(displayDetailsService != null)
        {
            byte[] globalAccountImage
                = displayDetailsService.getGlobalDisplayAvatar();

            if((globalAccountImage != null) && (globalAccountImage.length > 0))
                setPeerImage(globalAccountImage);

            globalDisplayName = displayDetailsService.getGlobalDisplayName();
        }

        ResourceManagementService resources = GuiActivator.getResources();

        setPeerName(
                (globalDisplayName != null && globalDisplayName.length() > 0)
                ? globalDisplayName
                    + " ("
                    + call.getProtocolProvider().getAccountID().getDisplayName()
                    + ")"
                : call.getProtocolProvider().getAccountID().getDisplayName());

        setTitleBackground(
                video
                    ? Color.DARK_GRAY
                    : new Color(
                            resources.getColor(
                                    "service.gui.CALL_LOCAL_USER_BACKGROUND")));

        if(isSoundLevelIndicatorEnabled())
            call.addLocalUserSoundLevelListener(soundLevelListener);
    }

    /**
     * Initializes a new <tt>ConferencePeerPanel</tt> which is to depict a
     * specific <tt>CallPeer</tt> on behalf of a specific
     * <tt>BasicConferenceCallPanel</tt> i.e. <tt>CallRenderer</tt>.
     *
     * @param callRenderer the <tt>BasicConferenceCallPanel</tt> which requests
     * the initialization of the new instance and which will use the new
     * instance to depict the specified <tt>CallPeer</tt>
     * @param callPeer the <tt>CallPeer</tt> to be depicted by the new instance
     */
    public ConferencePeerPanel(
            BasicConferenceCallPanel callRenderer,
            CallPeer callPeer)
    {
        this(callRenderer, callPeer, false);
    }

    /**
     * Initializes a new <tt>ConferencePeerPanel</tt> which is to depict a
     * specific <tt>CallPeer</tt> on behalf of a specific
     * <tt>BasicConferenceCallPanel</tt> i.e. <tt>CallRenderer</tt>.
     *
     * @param callRenderer the <tt>BasicConferenceCallPanel</tt> which requests
     * the initialization of the new instance and which will use the new
     * instance to depict the specified <tt>CallPeer</tt>
     * @param callPeer the <tt>CallPeer</tt> to be depicted by the new instance
     * @param video <tt>true</tt> if the new instance will be associated with a
     * display of video (e.g. which will be streaming from the specified
     * <tt>callPeer</tt>); otherwise, <tt>false</tt>
     * @throws NullPointerException if <tt>callPeer</tt> is <tt>null</tt>
     */
    public ConferencePeerPanel(
            BasicConferenceCallPanel callRenderer,
            CallPeer callPeer,
            boolean video)
    {
        super(callRenderer, callPeer, video);

        if (callPeer == null)
            throw new NullPointerException("callPeer");

        this.call = null;
        this.callPeer = callPeer;

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

        setMute(callPeer.isMute());

        setPeerImage(CallManager.getPeerImage(callPeer));
        setPeerName(callPeer.getDisplayName());

        // We initialize the status bar for call peers only.
        initStatusBar(callPeer);

        callPeerMenu = new CallPeerMenu(callPeer, callRenderer);

        SIPCommMenuBar menuBar = new SIPCommMenuBar();

        menuBar.setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0));
        menuBar.add(callPeerMenu);
        addToNameBar(menuBar);

        setTitleBackground(
                video
                    ? Color.DARK_GRAY
                    : new Color(
                            GuiActivator.getResources().getColor(
                                    "service.gui.CALL_PEER_NAME_BACKGROUND")));

        initSecuritySettings();

        callPeerAdapter = new CallPeerAdapter(this.callPeer, this);

        if(isSoundLevelIndicatorEnabled())
            this.callPeer.addStreamSoundLevelListener(soundLevelListener);
    }

    /**
     * {@inheritDoc}
     */
    public void dispose()
    {
        if (callPeerAdapter != null)
            callPeerAdapter.dispose();
        if (callPeer != null)
        {
            callPeer.removeConferenceMembersSoundLevelListener(
                    soundLevelListener);
            callPeer.removeStreamSoundLevelListener(soundLevelListener);
        }
        if (call != null)
            call.removeLocalUserSoundLevelListener(soundLevelListener);
    }

    /**
     * Gets the <tt>Call</tt> associated with this instance. If this instance
     * depicts the local peer, it was initialized with a specific <tt>Call</tt>
     * which represents the local peer. If this instance depicts an actual
     * <tt>CallPeer</tt>, its associated <tt>Call</tt> is returned.
     *
     * @return the <tt>Call</tt> associated with this instance
     */
    public Call getCall()
    {
        return (callPeer == null) ? call : callPeer.getCall();
    }

    /**
     * Returns <tt>CallPeer</tt> contact address.
     *
     * @return <tt>CallPeer</tt> contact address
     */
    public String getCallPeerContactAddress()
    {
        return callPeer.getURI();
    }

    /**
     * {@inheritDoc}
     *
     * Returns this instance.
     */
    public Component getComponent()
    {
        return this;
    }

    /**
     * Initializes the security settings for this call peer.
     */
    private void initSecuritySettings()
    {
        securityStatusLabel.setSecurityOff();

        CallPeerSecurityStatusEvent securityEvent
            = callPeer.getCurrentSecuritySettings();

        if (securityEvent != null
            && securityEvent instanceof CallPeerSecurityOnEvent)
        {
            CallPeerSecurityOnEvent securityOnEvt
                = (CallPeerSecurityOnEvent) securityEvent;

            securityOn(securityOnEvt);
        }

        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)
            {
                setSecurityPanelVisible(
                        !getCallPanel().getCallWindow().getFrame()
                                .getGlassPane().isVisible());
            }
        });
    }

    /**
     * Initializes the status bar component for the given <tt>callPeer</tt>.
     *
     * @param callPeer the underlying peer, which status would be displayed
     */
    private void initStatusBar(CallPeer callPeer)
    {
        initSecurityStatusLabel();
        this.setParticipantState(callPeer.getState().getLocalizedStateString());

        this.addToStatusBar(holdStatusLabel);
        this.addToStatusBar(muteStatusLabel);
        this.addToStatusBar(dtmfLabel);
    }

    /**
     * Indicates if the local video component is currently visible.
     *
     * @return <tt>true</tt> if the local video component is currently visible,
     * <tt>false</tt> - otherwise
     */
    public boolean isLocalVideoVisible()
    {
        return false;
    }

    /**
     * Determines whether the indicator which depicts the sound/audio levels (of
     * the local or remote peer in a call) is to be enabled. For example, the
     * indicator may be disabled for performance-related reasons.
     *
     * @return <tt>true</tt> if the indicator which depicts the sound/audio
     * levels (of the local or remote peer in a call) is to be enabled;
     * otherwise, <tt>false</tt>
     */
    static boolean isSoundLevelIndicatorEnabled()
    {
        return
            !GuiActivator.getConfigurationService().getBoolean(
                    "net.java.sip.communicator.impl.gui.main.call"
                        + ".DISABLE_SOUND_LEVEL_INDICATORS",
                    false);
    }

    /**
     * Reloads style information.
     */
    @Override
    public void loadSkin()
    {
        setTitleBackground(
                new Color(
                        GuiActivator.getResources().getColor(
                                "service.gui.CALL_LOCAL_USER_BACKGROUND")));

        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)));
        }
        if(callPeerMenu != null)
            callPeerMenu.loadSkin();
    }

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

    /**
     * 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
                = getCallPanel().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 securityNegotiationStartedEvent
     *            the security started event received
     */
    public void securityNegotiationStarted(
        CallPeerSecurityNegotiationStartedEvent securityNegotiationStartedEvent)
    {

    }

    /**
     * Indicates that the security has gone off.
     *
     * @param evt Details about the event that caused this message.
     */
    @Override
    public void securityOff(CallPeerSecurityOffEvent evt)
    {
        super.securityOff(evt);

        if(securityPanel != null)
        {
            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.
     */
    @Override
    public void securityOn(CallPeerSecurityOnEvent evt)
    {
        super.securityOn(evt);

        // If the securityOn is called without a specific event, we'll just call
        // the super and we'll return.
        if (evt == null)
            return;

        SrtpControl srtpControl = evt.getSecurityController();

        // In case this is the local peer.
        if (securityPanel == null)
            return;

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

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

        securityPanel.securityOn(evt);

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

        if (srtpControl instanceof ZrtpControl
            && !((ZrtpControl) srtpControl).isSecurityVerified()
            && !isSecurityLowPriority)
        {
            setSecurityPanelVisible(true);
        }
    }

    /**
     * Indicates that the security status is pending confirmation.
     */
    @Override
    public void securityPending()
    {
        super.securityPending();
    }

    /**
     * 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(CallPeerSecurityTimeoutEvent evt)
    {
        if(Boolean.parseBoolean(GuiActivator.getResources()
                .getSettingsString("impl.gui.PARANOIA_UI")))
        {
            try
            {
                CallPeer peer = (CallPeer) evt.getSource();
                OperationSetBasicTelephony<?> telephony
                    = peer.getProtocolProvider().getOperationSet(
                            OperationSetBasicTelephony.class);

                telephony.hangupCallPeer(
                    peer,
                    OperationSetBasicTelephony.HANGUP_REASON_ENCRYPTION_REQUIRED,
                    "Encryption Required!");
            }
            catch(OperationFailedException ex)
            {
                Logger.getLogger(getClass())
                    .error("Failed to hangup peer", ex);
            }
        }
    }

    /**
     * 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
     */
    @Override
    public void setErrorReason(String reason)
    {
        super.setErrorReason(reason);
    }

    /**
     * Shows/hides the local video component.
     *
     * @param isVisible <tt>true</tt> to show the local video, <tt>false</tt> -
     * otherwise
     */
    public void setLocalVideoVisible(boolean isVisible) {}


    /**
     * Sets the mute status icon to the status panel.
     *
     * @param isMute indicates if the call with this peer is
     * muted
     */
    public void setMute(boolean isMute)
    {
        if(isMute)
            muteStatusLabel.setIcon(new ImageIcon(
                ImageLoader.getImage(ImageLoader.MUTE_STATUS_ICON)));
        else
            muteStatusLabel.setIcon(null);

        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(boolean isOnHold)
    {
        if(isOnHold)
            holdStatusLabel.setIcon(new ImageIcon(
                ImageLoader.getImage(ImageLoader.HOLD_STATUS_ICON)));
        else
            holdStatusLabel.setIcon(null);

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

    /**
     * Sets the <tt>icon</tt> of the peer.
     * @param icon the icon to set
     */
    public void setPeerImage(byte[] icon)
    {
        // If this is the local peer (i.e. us) or any peer, but the focus peer.
        if (callPeer == null || !callPeer.isConferenceFocus())
            setParticipantImage(icon);
    }

    /**
     * Sets the name of the peer.
     * @param name the name of the peer
     */
    public void setPeerName(String name)
    {
        setParticipantName(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(   CallPeerState oldState,
                                CallPeerState newState,
                                String stateString)
    {
        this.setParticipantState(stateString);
    }

    /**
     * Shows/hides the security panel.
     *
     * @param isVisible <tt>true</tt> to show the security panel, <tt>false</tt>
     * to hide it
     */
    public void setSecurityPanelVisible(boolean isVisible)
    {
        final JFrame callFrame = getCallPanel().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, callFrame.getWidth(), 110);

            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);
                        }
                    }
                });
        }
    }

    /**
     * Implements the various types of listeners which get notified about
     * changes in the sound/audio levels of the model of this
     * <tt>ConferencePeerPanel</tt> and updates its sound level indicator.
     */
    private class SoundLevelListenerImpl
        implements ConferenceMembersSoundLevelListener,
                   SoundLevelListener
    {
        /**
         * {@inheritDoc}
         */
        public void soundLevelChanged(ConferenceMembersSoundLevelEvent ev)
        {
            /*
             * If the callPeer depicted by this ConferencePeerPanel instance is
             * represented as a ConferenceMember, update the sound level
             * indicator of this ConferencePeerPanel instance with the specified
             * sound level (value).
             */
            for (Map.Entry<ConferenceMember,Integer> e
                    : ev.getLevels().entrySet())
            {
                if (CallManager.addressesAreEqual(
                        e.getKey().getAddress(),
                        callPeer.getAddress()))
                {
                    updateSoundBar(e.getValue());
                    break;
                }
            }
        }

        /**
         * {@inheritDoc}
         */
        public void soundLevelChanged(Object source, int level)
        {
            if (source.equals(participant))
            {
                /*
                 * If the remote peer is a conference focus and there is at
                 * least one other member (i.e. different than the remote peer
                 * and the local peer/user), we expect the remote peer to send
                 * us CSRC-based audio levels. Otherwise, the stream-based audio
                 * levels may conflict with the CSRC-based audio levels.
                 */
                if (callPeer != null)
                {
                    int conferenceMemberCount
                        = callPeer.getConferenceMemberCount();

                    if (conferenceMemberCount > 2)
                        return;
                }

                updateSoundBar(level);
            }
        }
    }
}
