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

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

import javax.swing.*;

import net.java.sip.communicator.impl.gui.*;
import net.java.sip.communicator.impl.gui.event.*;
import net.java.sip.communicator.service.gui.*;
import net.java.sip.communicator.service.gui.Container;
import net.java.sip.communicator.util.*;

import org.osgi.framework.*;

/**
 * Provides capabilities to a specific <code>JComponent</code> to contain
 * <code>PluginComponent</code>s, track when they are added and removed.
 *
 * @author Lyubomir Marinov
 */
public class PluginContainer
    implements PluginComponentListener
{
    /**
     * The <tt>Logger</tt> used by the <tt>PluginContainer</tt> class and its
     * instances for logging output.
     */
    private static final Logger logger
        = Logger.getLogger(PluginContainer.class);

    /**
     * The <code>JComponent</code> which contains the components of the
     * <code>PluginComponent</code>s managed by this instance.
     */
    private final JComponent container;

    /**
     * The container id of the <code>PluginComponent</code> managed by this
     * instance.
     */
    private final Container containerId;

    /**
     * The list of <code>PluginComponent</code> instances which have their
     * components added to this <code>PluginContainer</code>.
     */
    private final java.util.List<PluginComponent> pluginComponents
        = new LinkedList<PluginComponent>();

    /**
     * Initializes a new <code>PluginContainer</code> instance which is to
     * provide capabilities to a specific <code>JComponent</code> container with
     * a specific <code>Container</code> id to contain
     * <code>PluginComponent</code> and track when they are added and removed.
     *
     * @param container
     *            the <code>JComponent</code> container the new instance is to
     *            provide its capabilities to
     * @param containerId
     *            the <code>Container</code> id of the specified
     *            <code>container</code>
     */
    public PluginContainer(JComponent container, Container containerId)
    {
        this.container = container;
        this.containerId = containerId;

        initPluginComponents();
    }

    /**
     * Adds a specific <tt>Component</tt> to a specific <tt>JComponent</tt>
     * container. Allows extenders to apply custom logic to the exact placement
     * of the specified <tt>Component</tt> in the specified container.
     *
     * @param component the <tt>Component</tt> to be added to the specified
     * <tt>JComponent</tt> container
     * @param container the <tt>JComponent</tt> container to add the specified
     * <tt>Component</tt> to
     * @param preferredIndex the index at which <tt>component</tt> is to be
     * added to <tt>container</tt> if possible or <tt>-1</tt> if there is no
     * preference with respect to the index in question
     */
    protected void addComponentToContainer(
            Component component,
            JComponent container,
            int preferredIndex)
    {
        if ((0 <= preferredIndex)
                && (preferredIndex < getComponentCount(container)))
            container.add(component, preferredIndex);
        else
            container.add(component);
    }

    /**
     * Adds the component of a specific <tt>PluginComponent</tt> to the
     * associated <tt>Container</tt>.
     *
     * @param factory the <tt>PluginComponentFactory</tt> which is to have its
     *          component added to the <tt>Container</tt> associated with this
     *          <tt>PluginContainer</tt>
     */
    private synchronized void addPluginComponent(PluginComponentFactory factory)
    {
        PluginComponent c =
            factory.getPluginComponentInstance(PluginContainer.this);

        if (logger.isInfoEnabled())
            logger.info("Will add plugin component: " + c);

        /*
         * Try to respect positionIndex of PluginComponent to some extent:
         * PluginComponents with positionIndex equal to 0 go at the beginning,
         * these with positionIndex equal to -1 follow them and then go these
         * with positionIndex greater than 0.
         */
        int cIndex = factory.getPositionIndex();
        int index = -1;
        int i = 0;

        for (PluginComponent pluginComponent : pluginComponents)
        {
            if (pluginComponent.equals(c))
                return;

            if (-1 == index)
            {
                int pluginComponentIndex = factory.getPositionIndex();

                if ((0 == cIndex) || (-1 == cIndex))
                {
                    if ((0 != pluginComponentIndex)
                            && (cIndex != pluginComponentIndex))
                        index = i;
                }
                else if (cIndex < pluginComponentIndex)
                    index = i;
            }

            i++;
        }

        int pluginComponentCount = pluginComponents.size();

        if (-1 == index)
            index = pluginComponents.size();

        /*
         * The container may have added Components of its own apart from the
         * ones this PluginContainer has added to it. Since the common case for
         * the additional Components is to have them appear at the beginning,
         * adjust the index so it gets correct in the common case.
         */
        int containerComponentCount = getComponentCount(container);

        addComponentToContainer(
                (Component) c.getComponent(),
                container,
                (containerComponentCount > pluginComponentCount)
                    ? (index + (containerComponentCount - pluginComponentCount))
                    : index);
        pluginComponents.add(index, c);

        container.revalidate();
        container.repaint();
    }

    /**
     * Runs clean-up for associated resources which need explicit disposal (e.g.
     * listeners keeping this instance alive because they were added to the
     * model which operationally outlives this instance).
     */
    public void dispose()
    {
        GuiActivator.getUIService().removePluginComponentListener(this);

        /*
         * Explicitly remove the components of the PluginComponent instances
         * because the latter are registered with OSGi and are thus global.
         */
        synchronized (this)
        {
            for (PluginComponent pluginComponent : pluginComponents)
                container.remove((Component) pluginComponent.getComponent());
            pluginComponents.clear();
        }
    }

    /**
     * Gets the number of <tt>Component</tt>s in a specific <tt>JComponent</tt>
     * container. For example, returns the result of
     * <tt>getMenuComponentCount()</tt> if <tt>container</tt> is an instance of
     * <tt>JMenu</tt>.
     *
     * @param container the <tt>JComponent</tt> container to get the number of
     * <tt>Component</tt>s of
     * @return the number of <tt>Component</tt>s in the specified
     * <tt>container</tt>
     */
    protected int getComponentCount(JComponent container)
    {
        return
            (container instanceof JMenu)
                ? ((JMenu) container).getMenuComponentCount()
                : container.getComponentCount();
    }

    /**
     * Gets the <tt>PluginComponent</tt>s of this <tt>PluginContainer</tt>.
     *
     * @return an <tt>Iterable</tt> over the <tt>PluginComponent</tt>s of this
     * <tt>PluginContainer</tt>
     */
    public Iterable<PluginComponent> getPluginComponents()
    {
        return pluginComponents;
    }

    /**
     * Adds the <tt>Component</tt>s of the <tt>PluginComponent</tt>s registered
     * in the OSGi <tt>BundleContext</tt> in the associated <tt>Container</tt>.
     */
    private void initPluginComponents()
    {
        GuiActivator.getUIService().addPluginComponentListener(this);

        // Look for PluginComponents registered in the OSGi BundleContext.
        ServiceReference[] serRefs = null;

        try
        {
            serRefs
                = GuiActivator
                    .bundleContext
                        .getServiceReferences(
                            PluginComponentFactory.class.getName(),
                            "("
                                + Container.CONTAINER_ID
                                + "="
                                + containerId.getID()
                                + ")");
        }
        catch (InvalidSyntaxException exc)
        {
            logger.error("Could not obtain plugin reference.", exc);
        }

        if (serRefs != null)
        {
            for (ServiceReference serRef : serRefs)
            {
                PluginComponentFactory factory
                    = (PluginComponentFactory)
                        GuiActivator.bundleContext.getService(serRef);

                addPluginComponent(factory);
            }
        }
    }

    /**
     * Implements
     * {@link PluginComponentListener#pluginComponentAdded(PluginComponentEvent)}.
     *
     * @param event a <tt>PluginComponentEvent</tt> which specifies the
     * <tt>PluginComponent</tt> which has been added
     */
    public void pluginComponentAdded(PluginComponentEvent event)
    {
        PluginComponentFactory factory = event.getPluginComponentFactory();

        if (factory.getContainer().equals(containerId))
            addPluginComponent(factory);
    }

    /**
     * Implements
     * {@link PluginComponentListener#pluginComponentRemoved(PluginComponentEvent)}.
     *
     * @param event a <tt>PluginComponentEvent</tt> which specifies the
     * <tt>PluginComponent</tt> which has been added
     */
    public void pluginComponentRemoved(PluginComponentEvent event)
    {
        PluginComponentFactory factory = event.getPluginComponentFactory();

        if (factory.getContainer().equals(containerId))
            removePluginComponent(factory);
    }

    /**
     * Removes the component of a specific <code>PluginComponent</code> from
     * this <code>PluginContainer</code>.
     *
     * @param factory
     *            the <code>PluginComponent</code> which is to have its
     *            component removed from this <code>PluginContainer</code>
     */
    private synchronized void removePluginComponent(
        PluginComponentFactory factory)
    {
        Iterator<PluginComponent> iterator = pluginComponents.iterator();
        while(iterator.hasNext())
        {
            PluginComponent c = iterator.next();
            if(c.getParentFactory().equals(factory))
            {
                iterator.remove();
                container.remove((Component)c.getComponent());
            }
        }
    }
}
