/*******************************************************************************
 * Copyright (c) 2000, 2016 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.jface.action;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.CoolBar;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.ToolBar;

/**
 * A <code>SubMenuManager</code> is used to define a set of contribution
 * items within a parent manager.  Once defined, the visibility of the entire set can
 * be changed as a unit.
 * <p>
 * A client may ask for and make additions to a submenu.  The visibility of these items
 * is also controlled by the visibility of the <code>SubMenuManager</code>.
 * </p>
 */
public class SubMenuManager extends SubContributionManager implements
        IMenuManager {

    /**
     * Maps each submenu in the manager to a wrapper.  The wrapper is used to
     * monitor additions and removals.  If the visibility of the manager is modified
     * the visibility of the submenus is also modified.
     */
    private Map<IMenuManager, SubMenuManager> mapMenuToWrapper;

    /**
     * List of registered menu listeners (element type: <code>IMenuListener</code>).
     */
	private ListenerList<IMenuListener> menuListeners = new ListenerList<>();

    /**
     * The menu listener added to the parent.  Lazily initialized
     * in addMenuListener.
     */
    private IMenuListener menuListener;

    /**
     * Constructs a new manager.
     *
     * @param mgr the parent manager.  All contributions made to the
     *      <code>SubMenuManager</code> are forwarded and appear in the
     *      parent manager.
     */
    public SubMenuManager(IMenuManager mgr) {
        super(mgr);
    }

    @Override
	public void addMenuListener(IMenuListener listener) {
        menuListeners.add(listener);
        if (menuListener == null) {
            menuListener = manager -> {
				for (IMenuListener localListener : menuListeners) {
					localListener.menuAboutToShow(SubMenuManager.this);
			    }
			};
        }
        getParentMenuManager().addMenuListener(menuListener);
    }

    /**
     * The default implementation of this <code>IContributionItem</code>
     * method does nothing. Subclasses may override.
     */
    @Override
	public void dispose() {
        // do nothing
    }

    @Override
	public void disposeManager() {
        if (menuListener != null) {
            getParentMenuManager().removeMenuListener(menuListener);
            menuListener = null;
            menuListeners.clear();
        }
        // Dispose wrapped menus in addition to removing them.
        // See bugs 64024 and 73715 for details.
        // important to dispose menu wrappers before call to super,
        // otherwise super's call to removeAll will remove them
        // before they can be disposed
        if (mapMenuToWrapper != null) {
            Iterator<SubMenuManager> iter = mapMenuToWrapper.values().iterator();
            while (iter.hasNext()) {
                SubMenuManager wrapper = iter.next();
                wrapper.disposeManager();
            }
            mapMenuToWrapper.clear();
            mapMenuToWrapper = null;
        }
        super.disposeManager();
    }

    @Override
	public void fill(Composite parent) {
        if (isVisible()) {
			getParentMenuManager().fill(parent);
		}
    }

    @Override
	public void fill(CoolBar parent, int index) {
        // do nothing
    }

    @Override
	public void fill(Menu parent, int index) {
        if (isVisible()) {
			getParentMenuManager().fill(parent, index);
		}
    }

    @Override
	public void fill(ToolBar parent, int index) {
        if (isVisible()) {
			getParentMenuManager().fill(parent, index);
		}
    }

	/**
	 * Method declared on IContributionManager.
	 *
	 * Returns the item passed to us, not the wrapper. In the case of menu's not
	 * added by this manager, ensure that we return a wrapper for the menu.
	 *
	 * @param id
	 *            of the {@link IContributionItem}
	 */
    @Override
	public IContributionItem find(String id) {
        IContributionItem item = getParentMenuManager().find(id);
        if (item instanceof SubContributionItem) {
			// Return the item passed to us, not the wrapper.
            item = unwrap(item);
		}

        if (item instanceof IMenuManager) {
            // if it is a menu manager wrap it before returning
            IMenuManager menu = (IMenuManager) item;
            item = getWrapper(menu);
        }

        return item;
    }

    /**
     * <p>
     * The menu returned is wrapped within a <code>SubMenuManager</code> to
     * monitor additions and removals.  If the visibility of this menu is modified
     * the visibility of the submenus is also modified.
     * </p>
     */
    @Override
	public IMenuManager findMenuUsingPath(String path) {
        IContributionItem item = findUsingPath(path);
        if (item instanceof IMenuManager) {
            return (IMenuManager) item;
        }
        return null;
    }

	/**
	 * Method declared on IMenuManager.
	 *
	 * Returns the item passed to us, not the wrapper.
	 *
	 * We use use the same algorithm as MenuManager.findUsingPath, but unwrap
	 * submenus along so that SubMenuManagers are visible.
	 *
	 * @param path
	 *            which allows to find an {@link IContributionItem} through the
	 *            menu structure.
	 */
    @Override
	public IContributionItem findUsingPath(String path) {
        String id = path;
        String rest = null;
        int separator = path.indexOf('/');
        if (separator != -1) {
            id = path.substring(0, separator);
            rest = path.substring(separator + 1);
        }
        IContributionItem item = find(id); // unwraps item
        if (rest != null && item instanceof IMenuManager) {
            IMenuManager menu = (IMenuManager) item;
            item = menu.findUsingPath(rest);
        }
        return item;
    }

    @Override
	public String getId() {
        return getParentMenuManager().getId();
    }

    /**
     * @return the parent menu manager that this sub-manager contributes to.
     */
    protected final IMenuManager getParentMenuManager() {
        // Cast is ok because that's the only
        // thing we accept in the construtor.
        return (IMenuManager) getParent();
    }

    @Override
	public boolean getRemoveAllWhenShown() {
        return false;
    }

    /**
     * Returns the menu wrapper for a menu manager.
     * <p>
     * The sub menus within this menu are wrapped within a <code>SubMenuManager</code> to
     * monitor additions and removals.  If the visibility of this menu is modified
     * the visibility of the sub menus is also modified.
     * <p>
     * @param mgr the menu manager to be wrapped
     *
     * @return the menu wrapper
     */
    protected IMenuManager getWrapper(IMenuManager mgr) {
        if (mapMenuToWrapper == null) {
            mapMenuToWrapper = new HashMap<>(4);
        }
        SubMenuManager wrapper = mapMenuToWrapper.get(mgr);
        if (wrapper == null) {
            wrapper = wrapMenu(mgr);
            mapMenuToWrapper.put(mgr, wrapper);
        }
        return wrapper;
    }

    @Override
	public boolean isDynamic() {
        return getParentMenuManager().isDynamic();
    }

    @Override
	public boolean isEnabled() {
        return isVisible() && getParentMenuManager().isEnabled();
    }

    @Override
	public boolean isGroupMarker() {
        return getParentMenuManager().isGroupMarker();
    }

    @Override
	public boolean isSeparator() {
        return getParentMenuManager().isSeparator();
    }

    /**
     * Remove all contribution items.
     */
    @Override
	public void removeAll() {
        super.removeAll();
        if (mapMenuToWrapper != null) {
            Iterator<SubMenuManager> iter = mapMenuToWrapper.values().iterator();
            while (iter.hasNext()) {
                SubMenuManager wrapper = iter.next();
                wrapper.removeAll();
            }
            mapMenuToWrapper.clear();
            mapMenuToWrapper = null;
        }
    }

    @Override
	public void removeMenuListener(IMenuListener listener) {
        menuListeners.remove(listener);
    }

    @Override
	public void saveWidgetState() {
        // do nothing
    }

    @Override
	public void setParent(IContributionManager parent) {
        // do nothing, our "parent manager's" parent
        // is set when it is added to a manager
    }

    @Override
	public void setRemoveAllWhenShown(boolean removeAll) {
        Assert.isTrue(false, "Should not be called on submenu manager"); //$NON-NLS-1$
    }

    @Override
	public void setVisible(boolean visible) {
        super.setVisible(visible);
        if (mapMenuToWrapper != null) {
            Iterator<SubMenuManager> iter = mapMenuToWrapper.values().iterator();
            while (iter.hasNext()) {
                SubMenuManager wrapper = iter.next();
                wrapper.setVisible(visible);
            }
        }
    }

    @Override
	public void update() {
        // This method is not governed by visibility.  The client may
        // call <code>setVisible</code> and then force an update.  At that
        // point we need to update the parent.
        getParentMenuManager().update();
    }

    @Override
	public void update(boolean force) {
        // This method is not governed by visibility.  The client may
        // call <code>setVisible</code> and then force an update.  At that
        // point we need to update the parent.
        getParentMenuManager().update(force);
    }

    @Override
	public void update(String id) {
        getParentMenuManager().update(id);
    }

    @Override
	public void updateAll(boolean force) {
        // This method is not governed by visibility.  The client may
        // call <code>setVisible</code> and then force an update.  At that
        // point we need to update the parent.
        getParentMenuManager().updateAll(force);
    }

    /**
     * Wraps a menu manager in a sub menu manager, and returns the new wrapper.
     * @param menu the menu manager to wrap
     * @return the new wrapped menu manager
     */
    protected SubMenuManager wrapMenu(IMenuManager menu) {
        SubMenuManager mgr = new SubMenuManager(menu);
        mgr.setVisible(isVisible());
        return mgr;
    }
}
