/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
 * or http://www.netbeans.org/cddl.txt.
 * 
 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */
package org.openide.actions;

import org.openide.actions.ActionManager;
import org.openide.awt.Actions;
import org.openide.util.ContextAwareAction;
import org.openide.util.HelpCtx;
import org.openide.util.Lookup;
import org.openide.util.NbBundle;
import org.openide.util.actions.*;

import java.beans.*;

import java.util.*;

import javax.swing.*;
import javax.swing.event.*;
import org.openide.awt.DynamicMenuContent;


/** A "meta-action" that displays (in a submenu) a list of enabled actions provided by modules.
* Such registered actions are called "service actions":
* they are provided externally but seem to provide additional services on existing components.
* Often they will be {@link NodeAction}s or {@link CookieAction}s so that they will
* be enabled based on the node selection, i.e. the node containing this popup.
* It is desirable for most nodes to include this action somewhere in their popup menu.
*
* <p><em>Note:</em> you do not need to touch this class to add a service action!
* Just add the action to a module manifest in an <code>Action</code> section.
*
* <p>The list of registered service actions is provided to this action from the implementation
* by means of {@link ActionManager}.
*
* @author Jaroslav Tulach
*/
public class ToolsAction extends SystemAction implements ContextAwareAction, Presenter.Menu, Presenter.Popup {
    static final long serialVersionUID = 4906417339959070129L;

    // Global ActionManager listener monitoring all available actions
    // and their state
    private static G gl;

    /** Lazy initialization of global listener.
     */
    private static synchronized G gl() {
        if (gl == null) {
            gl = new G();
        }

        return gl;
    }

    /* @return name
    */
    public String getName() {
        return getActionName();
    }

    /* @return help for this action
    */
    public HelpCtx getHelpCtx() {
        return new HelpCtx(ToolsAction.class);
    }

    /* @return menu presenter for the action
    */
    public JMenuItem getMenuPresenter() {
        return new Inline(this);
    }

    /* @return menu presenter for the action
    */
    public JMenuItem getPopupPresenter() {
        return new Popup(this);
    }

    /* Does nothing.
    */
    public void actionPerformed(java.awt.event.ActionEvent ev) {
        assert false;
    }

    public Action createContextAwareInstance(Lookup actionContext) {
        return new DelegateAction(this, actionContext);
    }

    /* @return name
    */
    private static String getActionName() {
        return NbBundle.getMessage(ToolsAction.class, "CTL_Tools");
    }

    /** Implementation method that regenerates the items in the menu or
    * in the array.
    *
    * @param forMenu true if will be presented in menu or false if presented in popup
    * @param list (can be null)
    */
    private static List generate(Action toolsAction, boolean forMenu) {
        ActionManager am = ActionManager.getDefault();
        SystemAction[] actions = am.getContextActions();
        List list = new ArrayList(actions.length);

        boolean separator = false;
        boolean firstItemAdded = false; // flag to prevent adding separator before actual menu items

        // Get action context.
        Lookup lookup;

        if (toolsAction instanceof Lookup.Provider) {
            lookup = ((Lookup.Provider) toolsAction).getLookup();
        } else {
            lookup = null;
        }

        for (int i = 0; i < actions.length; i++) {
            Action a;

            // Retrieve context sensitive action instance if possible.
            if ((lookup != null) && actions[i] instanceof ContextAwareAction) {
                a = ((ContextAwareAction) actions[i]).createContextAwareInstance(lookup);
            } else {
                a = actions[i];
            }

            if (a == null) {
                if (firstItemAdded) {
                    separator = true;
                }
            } else {
                boolean isPopup = (a instanceof Presenter.Popup);
                boolean isMenu = (a instanceof Presenter.Menu);

                if (!((forMenu && isMenu) || (!forMenu && isPopup)) && (isMenu || isPopup)) {
                    continue; // do not call isEnabled on action that is only popup presenter when building menu (i18nPopupAction)
                }

                if (a.isEnabled()) {
                    JMenuItem mi;

                    if (forMenu && isMenu) {
                        mi = ((Presenter.Menu) a).getMenuPresenter();
                    } else if (!forMenu && isPopup) {
                        mi = ((Presenter.Popup) a).getPopupPresenter();
                    } else if (!isMenu && !isPopup) {
                        // Generic Swing action.
                        mi = new JMenuItem();
                        Actions.connect(mi, a, !forMenu);
                    } else {
                        // Should not be here.
                        continue;
                    }

                    if (separator) {
                        list.add(null);
                        separator = false;
                    }

                    list.add(mi);
                    firstItemAdded = true;
                }
            }
        }

        return list;
    }

    //------------------------------------------

    /** @deprecated Useless, see {@link ActionManager}. */
    public static void setModel(Model m) {
        throw new SecurityException();
    }

    /** @deprecated Useless, see {@link ActionManager}. */
    public static interface Model {
        public SystemAction[] getActions();

        public void addChangeListener(javax.swing.event.ChangeListener l);

        public void removeChangeListener(javax.swing.event.ChangeListener l);
    }

    /** Inline menu that watches model changes only when really needed.
     */
    private static final class Inline extends JMenuItem implements DynamicMenuContent {
        static final long serialVersionUID = 2269006599727576059L;

        /** timestamp of the beginning of the last regeneration */
        private int timestamp = 0;

        /** Associated tools action. */
        private Action toolsAction;

        Inline(Action toolsAction) {
            this.toolsAction = toolsAction;
        }


        
        public JComponent[] synchMenuPresenters(JComponent[] items) {
            if (timestamp == gl().getTimestamp()) {
                return items;
            }
            // generate directly list of menu items
            List l = generate(toolsAction, true);
            timestamp = gl().getTimestamp();
            return (JMenuItem[]) l.toArray(new JMenuItem[l.size()]);
        }
        
        
        public JComponent[] getMenuPresenters() {
            return synchMenuPresenters(new JComponent[0]);
        }        
    }

    //--------------------------------------------------

    /** Inline menu that is either empty or contains one submenu.*/
    private static final class Popup extends JMenuItem implements DynamicMenuContent {
        static final long serialVersionUID = 2269006599727576059L;

        /** sub menu */
        private JMenu menu = new MyMenu();

        /** Associated tools action. */
        private Action toolsAction;

        public Popup(Action toolsAction) {
            super();
            this.toolsAction = toolsAction;
            HelpCtx.setHelpIDString(menu, ToolsAction.class.getName());

        }

        
        public JComponent[] synchMenuPresenters(JComponent[] items) {
            return gl().isPopupEnabled(toolsAction) ? new JMenuItem[] { menu } : new JMenuItem[0];
        }
        
        
        public JComponent[] getMenuPresenters() {
            return synchMenuPresenters(new JComponent[0]);
        }                


        /** A special menu that will properly update its submenu before posting */
        private class MyMenu extends org.openide.awt.JMenuPlus implements PopupMenuListener {
            /* A popup menu we've attached our listener to.
             * If null, the content is not up-to-date */
            private JPopupMenu lastPopup = null;

            MyMenu() {
                super(getActionName());
            }

            public JPopupMenu getPopupMenu() {
                JPopupMenu popup = super.getPopupMenu();
                fillSubmenu(popup);

                return popup;
            }

            private void fillSubmenu(JPopupMenu pop) {
                if (lastPopup == null) {
                    pop.addPopupMenuListener(this);
                    lastPopup = pop;

                    removeAll();

                    Iterator it = generate(toolsAction, false).iterator();

                    while (it.hasNext()) {
                        java.awt.Component item = (java.awt.Component) it.next();

                        if (item == null) {
                            addSeparator();
                        } else {
                            add(item);
                        }
                    }

                    // also work with empty element
                    if (getMenuComponentCount() == 0) {
                        JMenuItem empty = new JMenuItem(NbBundle.getMessage(ToolsAction.class, "CTL_EmptySubMenu"));
                        empty.setEnabled(false);
                        add(empty);
                    }
                }
            }

            public void popupMenuCanceled(PopupMenuEvent e) {
            }

            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
            }

            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
                lastPopup.removePopupMenuListener(this);
                lastPopup = null; // clear the status and stop listening
            }
        }
    }

    //------------------------------------------------
    //----------------------------------------------------------
    private static class G implements PropertyChangeListener {
        public static final String PROP_STATE = "actionsState"; // NOI18N
        private int timestamp = 1;
        private SystemAction[] actions = null;
        private PropertyChangeSupport supp = new PropertyChangeSupport(this);

        public G() {
            ActionManager am = ActionManager.getDefault();
            am.addPropertyChangeListener(this);
            actionsListChanged();
        }

        public final void addPropertyChangeListener(PropertyChangeListener listener) {
            supp.addPropertyChangeListener(listener);
        }

        public final void removePropertyChangeListener(PropertyChangeListener listener) {
            supp.removePropertyChangeListener(listener);
        }

        protected final void firePropertyChange(String name, Object o, Object n) {
            supp.firePropertyChange(name, o, n);
        }

        private void actionsListChanged() {
            timestamp++;

            // deregister all actions listeners
            SystemAction[] copy = actions;

            if (copy != null) {
                for (int i = 0; i < copy.length; i++) {
                    SystemAction act = copy[i];

                    if (act != null) {
                        act.removePropertyChangeListener(this);
                    }
                }
            }

            ActionManager am = ActionManager.getDefault();
            copy = am.getContextActions();

            for (int i = 0; i < copy.length; i++) {
                SystemAction act = copy[i];

                if (act != null) {
                    act.addPropertyChangeListener(this);
                }
            }

            actions = copy;

            firePropertyChange(PROP_STATE, null, null); // tell the world
        }

        private void actionStateChanged() {
            timestamp++;
            firePropertyChange(PROP_STATE, null, null); // tell the world
        }

        public void propertyChange(PropertyChangeEvent ev) {
            String prop = ev.getPropertyName();

            if ((prop == null) || prop.equals(ActionManager.PROP_CONTEXT_ACTIONS)) {
                actionsListChanged();
            } else if (prop.equals(SystemAction.PROP_ENABLED)) {
                actionStateChanged();
            }
        }

        /** Tells if there is any action that is willing to provide
         * Presenter.Popup
         */
        private boolean isPopupEnabled(Action toolsAction) {
            boolean en = false;
            SystemAction[] copy = actions;

            // Get action conext.
            Lookup lookup;

            if (toolsAction instanceof Lookup.Provider) {
                lookup = ((Lookup.Provider) toolsAction).getLookup();
            } else {
                lookup = null;
            }

            for (int i = 0; i < copy.length; i++) {
                // Get context aware action instance if needed.
                Action act;

                // Retrieve context aware action instance if possible.
                if ((lookup != null) && copy[i] instanceof ContextAwareAction) {
                    act = ((ContextAwareAction) copy[i]).createContextAwareInstance(lookup);
                } else {
                    act = copy[i];
                }

                if (act instanceof Presenter.Popup && act.isEnabled()) {
                    en = true;

                    break;
                }
            }

            return en;
        }

        private int getTimestamp() {
            return timestamp;
        }
    }

    /** Delegate tools action. Which act accordingly to current context
     * (represented by lookup). */
    private static final class DelegateAction extends Object implements Action, Presenter.Menu, Presenter.Popup,
        Lookup.Provider {
        private ToolsAction delegate;
        private Lookup lookup;

        /** support for listeners */
        private PropertyChangeSupport support = new PropertyChangeSupport(this);

        public DelegateAction(ToolsAction delegate, Lookup actionContext) {
            this.delegate = delegate;
            this.lookup = actionContext;
        }

        /** Overrides superclass method, adds delegate description. */
        public String toString() {
            return super.toString() + "[delegate=" + delegate + "]"; // NOI18N
        }

        /** Implements <code>Lookup.Provider</code>. */
        public Lookup getLookup() {
            return lookup;
        }

        public void actionPerformed(java.awt.event.ActionEvent e) {
        }

        public void putValue(String key, Object o) {
        }

        public Object getValue(String key) {
            return delegate.getValue(key);
        }

        public boolean isEnabled() {
            // Irrelevant see G#isPopupEnabled(..).
            return delegate.isEnabled();
        }

        public void setEnabled(boolean b) {
            // Irrelevant see G#isPopupEnabled(..).
        }

        public void addPropertyChangeListener(PropertyChangeListener listener) {
            support.addPropertyChangeListener(listener);
        }

        public void removePropertyChangeListener(PropertyChangeListener listener) {
            support.removePropertyChangeListener(listener);
        }

        /** Implements <code>Presenter.Menu</code>. */
        public javax.swing.JMenuItem getMenuPresenter() {
            return new Inline(this);
        }

        /** Implements <code>Presenter.Popup</code>. */
        public javax.swing.JMenuItem getPopupPresenter() {
            return new ToolsAction.Popup(this);
        }
    }
     // End of DelegateAction.
}
