/*
 * $Id: TargetManager.java 3972 2011-03-17 20:31:58Z kschaefe $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.jdesktop.swingx.action;

import java.awt.Component;
import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.JComponent;



/**
 * The target manager dispatches commands to {@link Targetable} objects
 * that it manages. This design of this class is based on the <i>Chain of
 * Responsiblity</i> and <i>Mediator</i> design patterns. The target manager
 * acts as a mediator between {@link TargetableAction}s and the intended targets.
 * This allows Action based components to invoke commands on components
 * without explicitly binding the user Action to the component action.
 * <p>
 * The target manager maintains a reference to a current
 * target and a target list.
 * The target list is managed using the <code>addTarget</code> and
 * <code>removeTarget</code> methods. The current target is managed using the
 * <code>setTarget</code> and <code>getTarget</code> methods.
 * <p>
 * Commands are dispatched to the Targetable objects in the <code>doCommand</code>
 * method in a well defined order. The doCommand method on the Targetable object
 * is called and if it returns true then the command has been handled and
 * command dispatching will stop. If the Targetable doCommand method returns
 * false then the
 * <p>
 * If none of the Targetable objects can handle the command then the default
 * behaviour is to retrieve an Action from the {@link javax.swing.ActionMap} of
 * the permanent focus owner with a key that matches the command key. If an
 * Action can be found then the <code>actionPerformed</code>
 * method is invoked using an <code>ActionEvent</code> that was constructed
 * using the command string.
 * <p>
 * If the Action is not found on the focus order then the ActionMaps of the ancestor
 * hierarchy of the focus owner is searched until a matching Action can be found.
 *  Finally, if none
 * of the components can handle the command then it is dispatched to the ActionMap
 * of the current Application instance.
 * <p>
 * The order of command dispatch is as follows:
 * <ul>
 *    <li>Current Targetable object invoking doCommand method
 *    <li>List order of Targetable objects invoking doCommand method
 *    <li>ActionMap entry of the permanent focus owner invoking actionPerfomed
 *    <li>ActionMap entry of the ancestor hierarchy of the permanent focus owner
 *    <li>ActionMap entry of the current Application instance
 * </ul>
 *
 * @see Targetable
 * @see TargetableAction
 * @author Mark Davidson
 */
public class TargetManager {

    private static TargetManager INSTANCE;
    private List<Targetable> targetList;
    private Targetable target;
    private PropertyChangeSupport propertySupport;

    /**
     * Create a target manager. Use this constructor if the application
     * may support many target managers. Otherwise, using the getInstance method
     * will return a singleton.
     */
    public TargetManager() {
        propertySupport = new PropertyChangeSupport(this);
    }

    /**
     * Return the singleton instance.
     */
    public static TargetManager getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new TargetManager();
        }
        return INSTANCE;
    }

    /**
     * Add a target to the target list. Will be appended
     * to the list by default. If the prepend flag is true then
     * the target will be added at the head of the list.
     * <p>
     * Targets added to the head of the list will will be the first
     * to handle the command.
     *
     * @param target the targeted object to add
     * @param prepend if true add at the head of the list; false append
     */
    public void addTarget(Targetable target, boolean prepend) {
        if (targetList == null) {
            targetList = new ArrayList<Targetable>();
        }
        if (prepend) {
            targetList.add(0, target);
        } else {
            targetList.add(target);
        }
        // Should add focus listener to the component.
    }

    /**
     * Appends the target to the target list.
     * @param target the targeted object to add
     */
    public void addTarget(Targetable target) {
        addTarget(target, false);
    }

    /**
     * Remove the target from the list
     */
    public void removeTarget(Targetable target) {
        if (targetList != null) {
            targetList.remove(target);
        }
    }

    /**
     * Returns an array of managed targets that were added with the
     * <code>addTarget</code> methods.
     *
     * @return all the <code>Targetable</code> added or an empty array if no
     *         targets have been added
     */
    public Targetable[] getTargets() {
        Targetable[] targets;
        if (targetList == null) {
            targets = new Targetable[0];
        } else {
            targets = new Targetable[targetList.size()];
            targets = (Targetable[])targetList.toArray(new Targetable[targetList.size()]);
        }
        return targets;
    }

    /**
     * Gets the current targetable component. May or may not
     * in the target list. If the current target is null then
     * the the current targetable component will be the first one
     * in the target list which can execute the command.
     *
     * This is a bound property and will fire a property change event
     * if the value changes.
     *
     * @param newTarget the current targetable component to set or null if
     *       the TargetManager shouldn't have a current targetable component.
     */
    public void setTarget(Targetable newTarget) {
        Targetable oldTarget = target;
        if (oldTarget != newTarget) {
            target = newTarget;
            propertySupport.firePropertyChange("target", oldTarget, newTarget);
        }
    }

    /**
     * Return the current targetable component. The curent targetable component
     * is the first place where commands will be dispatched.
     *
     * @return the current targetable component or null
     */
    public Targetable getTarget() {
        return target;
    }

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

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

    /**
     * Executes the command on the current targetable component.
     * If there isn't current targetable component then the list
     * of targetable components are searched and the first component
     * which can execute the command. If none of the targetable
     * components handle the command then the ActionMaps of the
     * focused components are searched.
     *
     * @param command the key of the command
     * @param value the value of the command; depends on context
     * @return true if the command has been handled otherwise false
     */
    public boolean doCommand(Object command, Object value) {
        // Try to invoked the explicit target.
        if (target != null) {
            if (target.hasCommand(command) && target.doCommand(command, value)) {
                return true;
            }
        }

        // The target list has the next chance to handle the command.
        if (targetList != null) {
            Iterator<Targetable> iter = targetList.iterator();
            while (iter.hasNext()) {
                Targetable target = iter.next();
                if (target.hasCommand(command) &&
                    target.doCommand(command, value)) {
                    return true;
                }
            }
        }

        ActionEvent evt = null;
        if (value instanceof ActionEvent) {
            evt = (ActionEvent)value;
        }

        // Fall back behavior. Get the component which has focus and search the
        // ActionMaps in the containment hierarchy for matching action.
        Component comp = KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner();
        while (comp != null) {
            if (comp instanceof JComponent) {
                ActionMap map = ((JComponent)comp).getActionMap();
                Action action = map.get(command);
                if (action != null) {
                    if (evt == null) {
                        evt = new ActionEvent(comp, 0, command.toString());
                    }
                    action.actionPerformed(evt);

                    return true;
                }
            }
            comp = comp.getParent();
        }

        return false;
    }

    /**
     * Resets the TargetManager.
     * This method is package private and for testing purposes only.
     */
    void reset() {
        if (targetList != null) {
            targetList.clear();
            targetList = null;
        }
        target = null;

        PropertyChangeListener[] listeners = propertySupport.getPropertyChangeListeners();
        for (int i = 0; i < listeners.length; i++) {
            propertySupport.removePropertyChangeListener(listeners[i]);
        }
        INSTANCE = null;
    }

}
