/*
 * @(#)CommandMenu.java 5.1
 *
 */

package CH.ifa.draw.util;

import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import javax.swing.AbstractButton;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JSeparator;
import javax.swing.KeyStroke;
import javax.swing.MenuElement;

import de.renew.draw.ui.ontology.AbstractCommand;

/**
 * A {@link AbstractCommand} enabled menu.  Selecting a <code>Command</code> menu
 * item executes the corresponding command.  <code>Command</code> menu
 * items are checked automatically for enabledness.
 * <br>
 * {@link AbstractCommand}
 */
public class CommandMenu extends JMenu implements ActionListener {
    /**
     * Logger for the class CommandMenu.
     * Only a single logger instance exists at any given time. If a logger for CommandMenu
     * already exists it is returned or else a new instance is created.
     */
    public static org.apache.log4j.Logger logger =
        org.apache.log4j.Logger.getLogger(CommandMenu.class);

    /**
     * Maps from commands to their associated CommandMenuItems. This
     * information is needed to be able to remove those items later on
     * request.
     **/
    private final Map<AbstractCommand, CommandMenuItem> _commandItems = new HashMap<>();

    /**
     * Constructs a new CommandMenu, which is a JMenu with all executable commands set to enabled.
     *
     * @param name the name for the CommandMenu
     */
    public CommandMenu(String name) {
        super(name);
        checkEnabled();
    }

    /**
     * Adds an item to the menu.  If the item is a {@link CommandMenuItem},
     * the command magic is enabled (see class <code>CommandMenu</code>
     * documentation).  It is not allowed to add the same command twice.
     *
     * @param item the <code>JMenuItem</code> to add to the menu.
     * @return the added <code>JMenuItem</code>.
     * @throws IllegalArgumentException
     *   if an item for the same command has already been added to the menu.
     **/
    @Override
    public synchronized JMenuItem add(JMenuItem item) {
        if (item instanceof CommandMenuItem citem) {
            AbstractCommand command = citem.getCommand();
            if (_commandItems.containsKey(command)) {
                throw new IllegalArgumentException(
                    "Cannot add command " + command.getCommandName()
                        + ": it already exists in the menu.");
            }
            _commandItems.put(command, citem);
            citem.addActionListener(this);
            JMenuItem result = super.add(citem);
            checkEnabled();
            return result;
        } else {
            return super.add(item);
        }
    }

    /**
     * Adds an item to the menu at the specified position.
     * If the item is a {@link CommandMenuItem}, the command magic is
     * enabled (see class <code>CommandMenu</code> documentation).
     * It is not allowed to add the same command twice.
     * <br>
     * The super method is defined for {@link Component}s, therefore
     * this method has to accept them and also return them.
     *
     * @param item the <code>Component</code> to add to the menu.
     * @return the added <code>Component</code>.
     * @throws IllegalArgumentException
     *   if an item for the same command has already been added to the menu.
     */
    @Override
    public synchronized Component add(Component item, int position) {
        if (item instanceof CommandMenuItem citem) {
            AbstractCommand command = citem.getCommand();
            if (_commandItems.containsKey(command)) {
                throw new IllegalArgumentException(
                    "Cannot add command " + command.getCommandName()
                        + ": it already exists in the menu.");
            }
            _commandItems.put(command, citem);
            citem.addActionListener(this);
            Component result = super.add(item, position);
            checkEnabled();
            return result;
        } else {
            return super.add(item, position);
        }
    }

    /**
     * Adds a command to the menu. The item's label is the command's
     * name. It is not allowed to add the same command twice.
     *
     * @param command the command that will be added
     */
    public synchronized void add(AbstractCommand command) {
        add(new CommandMenuItem(command));
    }

    /**
     * Adds a command with the given shortcut to the menu. The item's
     * label is the command's name. It is not allowed to add the same
     * command twice.
     *
     * @param command the command that will be added
     * @param shortcut the shortcut used to execute the command
     */
    public synchronized void add(AbstractCommand command, int shortcut) {
        add(new CommandMenuItem(command, shortcut));
    }

    /**
     * Adds a command with the given shortcut and a modifier to the menu.
     * The item's label is the command's name. It is not allowed to
     * add the same command twice.
     * This method allows support for different modifier keys other than the usual
     * <code>ctrl</code>-key
     *
     * @param command the command that will be added
     * @param shortcut the shortcut used to execute the command
     * @param modifier the modifier key used in the shortcut
     *                 (see {@link KeyStroke#getKeyStroke(int, int)})
     */
    public synchronized void add(AbstractCommand command, int shortcut, int modifier) {
        add(new CommandMenuItem(command, shortcut, modifier));
    }

    /**
     * Removes a command from the menu.
     *
     * @param command the <code>Command</code> to remove.
     * @throws NoSuchElementException
     *   if the given command that does not exist in the menu.
     **/
    public synchronized void remove(AbstractCommand command) {
        CommandMenuItem citem = _commandItems.get(command);
        if (citem == null) {
            throw new IllegalArgumentException(
                "Cannot remove command " + command.getCommandName()
                    + ": it does not exist in the menu.");
        } else {
            remove(citem);
        }
    }

    /**
     * Removes an item from the menu.
     * If the item is a {@link CommandMenuItem}, the command magic is removed, too.
     *
     * @param item the <code>JMenuItem</code> to remove.
     **/
    @Override
    public synchronized void remove(JMenuItem item) {
        if (item instanceof CommandMenuItem citem) {
            AbstractCommand command = citem.getCommand();
            if (_commandItems.get(command) != item) {
                throw new IllegalArgumentException(
                    "Cannot remove item for command " + command.getCommandName()
                        + ": the item is insconsistent with the menu.");
            }
            _commandItems.remove(command);
            citem.dispose();
            citem.removeActionListener(this);
        } else if (item instanceof CommandMenu menu) {
            for (MenuElement elem : menu.getPopupMenu().getSubElements()) {
                if (elem instanceof JMenuItem sub) {
                    menu.remove(sub);
                } else if (elem instanceof JPopupMenu.Separator sep) {
                    menu.remove(sep);
                }
            }
            menu.ui.uninstallUI(menu);
        }
        super.remove(item);
    }

    /**
     * Checks the enabled state of the menu components by looping over all MenuComponents and
     * testing whether the associated command is executable. If so, the component is set to
     * enabled.
     *
     * @return <code>true</code> if at least one menu component is enabled,<br>
     *         <code>false</code> else
     */
    public synchronized boolean checkEnabled() {
        boolean menuEnabled = false;
        boolean enabled;
        for (int i = 0; i < getMenuComponentCount(); i++) {
            Component item = getMenuComponent(i);
            if (item == null) {
                return false;
            }
            if (item instanceof CommandMenuItem) {
                enabled = ((CommandMenuItem) item).getCommand().isExecutable();
                item.setEnabled(enabled);
                menuEnabled = menuEnabled | enabled;
            } else if (item instanceof CommandMenu) {
                enabled = ((CommandMenu) item).checkEnabled();
                item.setEnabled(enabled);
                menuEnabled = menuEnabled | enabled;
            } else if (item instanceof JSeparator) {
                // Ignore separators.
            } else {
                menuEnabled = menuEnabled | item.isEnabled();
            }
        }
        return menuEnabled;
    }

    /**
     * Updates the text of CommandMenuItems to match their associated command names.
     * If a CommandMenu holds another CommandMenu, this function recursively updates the text of the
     * (sub-)CommandMenu.
     */
    public synchronized void updateCommandText() {
        String commandText;
        for (int i = 0; i < getMenuComponentCount(); i++) {
            Component item = getMenuComponent(i);
            if (item == null) {
                return;
            }
            if (item instanceof CommandMenuItem) {
                AbstractCommand command = ((CommandMenuItem) item).getCommand();
                commandText = command.getCommandName();
                String text = ((AbstractButton) item).getText();
                if (text != null && commandText != null && !text.equals(commandText)) {
                    ((AbstractButton) item).setText(commandText);
                }
            } else if (item instanceof CommandMenu) {
                ((CommandMenu) item).updateCommandText();
            }
        }
    }

    /**
     * Executes the command.
     */
    @Override
    public void actionPerformed(ActionEvent e) {
        Object source = e.getSource();
        if (source instanceof CommandMenuItem cmi) {
            AbstractCommand cmd = cmi.getCommand();
            cmd.execute();
        }
    }
}