/*
 * 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 javax.swing.*;

import net.java.sip.communicator.impl.gui.*;
import net.java.sip.communicator.plugin.desktoputil.*;

/**
 * The dialog created for a given call.
 *
 * @author Yana Stamcheva
 * @author Adam Netocny
 * @author Lyubomir Marinov
 */
public class CallDialog
    extends SIPCommFrame
    implements CallContainer,
               CallTitleListener
{
    /**
     * Serial version UID.
     */
    private static final long serialVersionUID = 0L;

    /**
     * Enabling force minimized mode will always open call dialog minimized.
     * The call dialog still can be shown, but by default it will be minimized.
     */
    private static final String FORCE_MINIMIZED_MODE
        = "net.java.sip.communicator.impl.gui.main.call.FORCE_MINIMIZED_MODE";

    /**
     * Finds a <tt>Container</tt> which is an ancestor of a specific
     * <tt>Component</tt>, has a set <tt>preferredSize</tt> and is closest to
     * the specified <tt>Component</tt> up the ancestor hierarchy.
     *
     * @param component the <tt>Component</tt> whose ancestor hierarchy is to be
     * searched upwards
     * @return a <tt>Container</tt>, if any, which is an ancestor of the
     * specified <tt>component</tt>, has a set <tt>preferredSize</tt> and is
     * closest to the specified <tt>component</tt> up the ancestor hierarchy
     */
    private static Container findClosestAncestorWithSetPreferredSize(
            Component component)
    {
        if ((component instanceof Container) && component.isPreferredSizeSet())
            return (Container) component;
        else
        {
            Container parent;

            while ((parent = component.getParent()) != null)
            {
                if (parent.isPreferredSizeSet())
                    return parent;
                else
                    component = parent;
            }
            return null;
        }
    }

    /**
     * The panel, where all call components are added.
     */
    private CallPanel callPanel;

    private final WindowStateListener windowStateListener
        = new WindowStateListener()
        {
            public void windowStateChanged(WindowEvent ev)
            {
                switch (ev.getID())
                {
                case WindowEvent.WINDOW_DEACTIVATED:
                case WindowEvent.WINDOW_ICONIFIED:
                case WindowEvent.WINDOW_LOST_FOCUS:
                    setFullScreen(false);
                    break;
                }
            }
        };

    /**
     * Creates a <tt>CallDialog</tt> by specifying the underlying call panel.
     */
    public CallDialog()
    {
        super(true, false);

        setMinimumSize(new Dimension(360, 300));
    }

    /**
     * Adds a call panel.
     *
     * @param callPanel the call panel to add to this dialog
     */
    public void addCallPanel(CallPanel callPanel)
    {
        this.callPanel = callPanel;

        getContentPane().add(callPanel);

        callPanel.addCallTitleListener(this);
        setTitle(callPanel.getCallTitle());

        if (!isVisible())
        {
            pack();

            // checks whether we need to open the call dialog in minimized mode
            if(GuiActivator.getConfigurationService()
                    .getBoolean(FORCE_MINIMIZED_MODE, false))
            {
                setState(ICONIFIED);
            }
            setVisible(true);
        }
    }

    /**
     * Called when the title of the given <tt>CallPanel</tt> changes.
     *
     * @param callPanel the <tt>CallPanel</tt>, which title has changed
     */
    public void callTitleChanged(CallPanel callPanel)
    {
        if (this.callPanel.equals(callPanel))
            setTitle(callPanel.getCallTitle());
    }

    /**
     * {@inheritDoc}
     *
     * Hang ups the call/telephony conference depicted by this
     * <tt>CallDialog</tt> on close.
     */
    @Override
    protected void close(boolean escape)
    {
        if (escape)
        {
            /*
             * In full-screen mode, ESC does not close this CallDialog but exits
             * from full-screen to windowed mode.
             */
            if (isFullScreen())
            {
                setFullScreen(false);
                return;
            }
        }
        else
        {
            /*
             * If the window has been closed by clicking the X button or
             * pressing the key combination corresponding to the same button we
             * close the window first and then perform all hang up operations.
             */

            callPanel.disposeCallInfoFrame();
            // We hide the window here. It will be disposed when the call has
            // been ended.
            setVisible(false);
        }

        // Then perform hang up operations.
        callPanel.actionPerformedOnHangupButton(escape);
    }

    /**
     * {@inheritDoc}
     *
     * The delay implemented by <tt>CallDialog</tt> is 5 seconds.
     */
    public void close(final CallPanel callPanel, boolean delay)
    {
        if (this.callPanel.equals(callPanel))
        {
            if (delay)
            {
                Timer timer
                    = new Timer(
                            5000,
                            new ActionListener()
                            {
                                public void actionPerformed(ActionEvent ev)
                                {
                                    dispose();
                                }
                            });

                timer.setRepeats(false);
                timer.start();
            }
            else
            {
                dispose();
            }
        }
    }

    /**
     * {@inheritDoc}
     *
     * <tt>CallDialog</tt> prepares the <tt>CallPanel</tt> it contains for
     * garbage collection.
     */
    @Override
    public void dispose()
    {
        super.dispose();

        /*
         * Technically, CallManager adds/removes the callPanel to/from this
         * instance. It may want to just move it to another CallContainer so it
         * does not sound right that we are disposing of it. But we do not have
         * such a case at this time so try to reduce the risk of memory leaks.
         */
        if (this.callPanel != null)
        {
            callPanel.disposeCallInfoFrame();
            callPanel.dispose();
        }
    }

    /**
     * {@inheritDoc}
     *
     * Attempts to adjust the size of this <tt>Frame</tt> as requested in the
     * AWT event dispatching thread.
     * <p>
     * The method may be executed on the AWT event dispatching thread only
     * because whoever is making the decision to request an adjustment of the
     * Frame size in relation to a AWT Component should be analyzing that same
     * Component in the AWT event dispatching thread only.
     * </p>
     *
     * @throws RuntimeException if the method is not called on the AWT event
     * dispatching thread
     */
    public void ensureSize(Component component, int width, int height)
    {
        CallManager.assertIsEventDispatchingThread();

        Frame frame = CallPeerRendererUtils.getFrame(component);

        if (frame == null)
            return;
        else if ((frame.getExtendedState() & Frame.MAXIMIZED_BOTH)
                == Frame.MAXIMIZED_BOTH)
        {
            /*
             * Forcing the size of a Component which is displayed in a maximized
             * window does not sound like anything we want to do.
             */
            return;
        }
        else if (frame.equals(
                frame.getGraphicsConfiguration().getDevice()
                        .getFullScreenWindow()))
        {
            /*
             * Forcing the size of a Component which is displayed in a
             * full-screen window does not sound like anything we want to do.
             */
            return;
        }
        else if (!frame.equals(this))
        {
            /* This Frame will try to adjust only its own size. */
            return;
        }
        else if ((component.getHeight() >= height)
                && (component.getWidth() >= width))
        {
            /*
             * We will only enlarge the frame size. If the component has already
             * been given at least what it is requesting, do not enlarge the
             * frame size because the whole calculation is prone to inaccuracy.
             */
            return;
        }
        else
        {
            /*
             * If there is no callPanel, it is unlikely that this CallDialog
             * will be asked to ensureSize. Anyway, support the scenario just in
             * case. In light of the absence of a callPanel to guide this
             * CallDialog about the preferred size, we do not have much of a
             * choice but to trust the method arguments.
             */
            if (callPanel != null)
            {
                /*
                 * If there is a callPanel, we are likely to get a much better
                 * estimation about the preferred size by asking the callPanel
                 * rather than by trusting the method arguments. For example,
                 * the visual Component displaying the video streaming from the
                 * local user/peer to the remote peer(s) will think that its
                 * preferred size is the one to base this Frame's size on but
                 * that may be misleading because the local video may not be
                 * displayed with its preferred size even if this Frame's size
                 * will accommodate it.
                 */
                /*
                 * Just asking the callPanel about its preferredSize would've
                 * been terrificly great. Unfortunately, that is presently
                 * futile because the callPanel may have a preferredSize while
                 * we are still required to display visual Components displaying
                 * video in their non-scaled size. The same goes for any
                 * Container which is an ancestor of the specified component.
                 */
                Container ancestor
                    = findClosestAncestorWithSetPreferredSize(component);

                if (ancestor == null)
                    ancestor = callPanel;
                /*
                 * If the ancestor has a forced preferredSize, its LayoutManager
                 * may be able to give a good enough estimation.
                 */
                if (ancestor.isPreferredSizeSet())
                {
                    LayoutManager ancestorLayout = ancestor.getLayout();

                    if (ancestorLayout != null)
                    {
                        Dimension preferredLayoutSize
                            = ancestorLayout.preferredLayoutSize(ancestor);

                        if (preferredLayoutSize != null)
                        {
                            component = ancestor;
                            width = preferredLayoutSize.width;
                            height = preferredLayoutSize.height;
                        }
                    }
                }
                else
                {
                    /*
                     * If the ancestor doesn't have a preferredSize forced, then
                     * we may think that it will calculate an appropriate
                     * preferredSize itself.
                     */
                    Dimension prefSize = ancestor.getPreferredSize();

                    if (prefSize != null)
                    {
                        component = ancestor;
                        width = prefSize.width;
                        height = prefSize.height;
                    }
                }
            }

            /*
             * If the component (which may be an ancestor of the Component
             * specified as an argument to the ensureSize method at this point)
             * has not been given a size, we will make a mistake if we try to
             * use it for the purposes of determining how much this Frame is to
             * be enlarged.
             */
            Dimension componentSize = component.getSize();

            if ((componentSize.width < 1) || (componentSize.height < 1))
                return;

            Dimension frameSize = frame.getSize();
            int newFrameWidth = frameSize.width + width - componentSize.width;
            int newFrameHeight
                = frameSize.height + height - componentSize.height;

            // Respect the minimum size.
            Dimension minSize = frame.getMinimumSize();

            if (newFrameWidth < minSize.width)
                newFrameWidth = minSize.width;
            if (newFrameHeight < minSize.height)
                newFrameHeight = minSize.height;

            // Don't get bigger than the screen.
            Rectangle screenBounds
                = frame.getGraphicsConfiguration().getBounds();

            if (newFrameWidth > screenBounds.width)
                newFrameWidth = screenBounds.width;
            if (newFrameHeight > screenBounds.height)
                newFrameHeight = screenBounds.height;

            /*
             * If we're going to make too small a change, don't even bother.
             * Besides, we don't want some weird recursive resizing.
             * Additionally, do not reduce the Frame size.
             */
            boolean changeWidth = ((newFrameWidth - frameSize.width) > 1);
            boolean changeHeight = ((newFrameHeight - frameSize.height) > 1);

            if (changeWidth || changeHeight)
            {
                if (!changeWidth)
                    newFrameWidth = frameSize.width;
                else if (!changeHeight)
                    newFrameHeight = frameSize.height;

                /*
                 * The latest requirement with respect to the behavior upon
                 * resizing is to center the Frame.
                 */
                int newFrameX
                    = screenBounds.x
                        + (screenBounds.width - newFrameWidth) / 2;
                int newFrameY
                    = screenBounds.y
                        + (screenBounds.height - newFrameHeight) / 2;

                // Do not let the top left go out of the screen.
                if (newFrameX < screenBounds.x)
                    newFrameX = screenBounds.x;
                if (newFrameY < screenBounds.y)
                    newFrameY = screenBounds.y;

                frame.setBounds(
                        newFrameX, newFrameY,
                        newFrameWidth, newFrameHeight);

                /*
                 * Make sure that the component which originally requested the
                 * update to the size of the frame realizes the change as soon
                 * as possible; otherwise, it may request yet another update.
                 */
                if (frame.isDisplayable())
                {
                    if (frame.isValid())
                        frame.doLayout();
                    else
                        frame.validate();
                    frame.repaint();
                }
                else
                    frame.doLayout();
            }
        }
    }

    /**
     * Returns the frame of the call window.
     *
     * @return the frame of the call window
     */
    public JFrame getFrame()
    {
        return this;
    }

    /**
     * Overrides getMinimumSize and checks the minimum size that
     * is needed to display buttons and use it for minimum size if
     * needed.
     * @return minimum size.
     */
    @Override
    public Dimension getMinimumSize()
    {
        Dimension minSize = super.getMinimumSize();

        if(callPanel != null)
        {
            int minButtonWidth = callPanel.getMinimumButtonWidth();

            if(minButtonWidth > minSize.getWidth())
                minSize = new Dimension(minButtonWidth, 300);
        }

        return minSize;
    }

    /**
     * Indicates if the given <tt>callPanel</tt> is currently visible.
     *
     * @param callPanel the <tt>CallPanel</tt>, for which we verify
     * @return <tt>true</tt> if the given call container is visible in this
     * call window, otherwise - <tt>false</tt>
     */
    public boolean isCallVisible(CallPanel callPanel)
    {
        return this.callPanel.equals(callPanel) ? isVisible() : false;
    }

    /**
     * {@inheritDoc}
     */
    public boolean isFullScreen()
    {
        return isFullScreen(getFrame());
    }

    /**
     * Determines whether a specific <tt>Window</tt> is displayed in full-screen
     * mode.
     *
     * @param window the <tt>Window</tt> to be checked whether it is displayed
     * in full-screen mode
     * @return <tt>true</tt> if the specified <tt>window</tt> is displayed in
     * full-screen mode; otherwise, <tt>false</tt>
     */
    public static boolean isFullScreen(Window window)
    {
        GraphicsConfiguration graphicsConfiguration
            = window.getGraphicsConfiguration();

        if (graphicsConfiguration != null)
        {
            GraphicsDevice device = graphicsConfiguration.getDevice();

            if (device != null)
                return window.equals(device.getFullScreenWindow());
        }
        return false;
    }

    /**
     * {@inheritDoc}
     */
    public void setFullScreen(boolean fullScreen)
    {
        GraphicsConfiguration graphicsConfiguration
            = getGraphicsConfiguration();

        if (graphicsConfiguration != null)
        {
            GraphicsDevice device = graphicsConfiguration.getDevice();

            if (device != null)
            {
                boolean thisIsFullScreen = equals(device.getFullScreenWindow());
                boolean firePropertyChange = false;
                boolean setVisible = isVisible();

                try
                {
                    if (fullScreen)
                    {
                        if (!thisIsFullScreen)
                        {
                            /*
                             * XXX The setUndecorated method will only work if
                             * this Window is not displayable.
                             */
                            windowDispose();
                            setUndecorated(true);

                            device.setFullScreenWindow(this);
                            firePropertyChange = true;
                        }
                    }
                    else if (thisIsFullScreen)
                    {
                        /*
                         * XXX The setUndecorated method will only work if this
                         * Window is not displayable.
                         */
                        windowDispose();
                        setUndecorated(false);

                        device.setFullScreenWindow(null);
                        firePropertyChange = true;
                    }

                    if (firePropertyChange)
                    {
                        if (fullScreen)
                        {
                            addWindowStateListener(windowStateListener);

                            /*
                             * If full-screen mode, a black background is the
                             * most common.
                             */
                            getContentPane().setBackground(Color.BLACK);
                        }
                        else
                        {
                            removeWindowStateListener(windowStateListener);

                            /*
                             * In windowed mode, a system-defined background is
                             * the most common.
                             */
                            getContentPane().setBackground(null);
                        }

                        firePropertyChange(
                            PROP_FULL_SCREEN,
                            thisIsFullScreen,
                            fullScreen);
                    }
                }
                finally
                {
                    /*
                     * Regardless of whether this Window successfully entered or
                     * exited full-screen mode, make sure that remains visible.
                     */
                    if (setVisible)
                        setVisible(true);
                }
            }
        }
    }
}
