
/*
 * Copyright (C) 2006 Sun Microsystems, Inc. All rights reserved. Use is
 * subject to license terms.
 */
package org.jdesktop.application;

import java.awt.KeyboardFocusManager;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.WeakHashMap;
import java.util.logging.Logger;
import javax.swing.ActionMap;
import javax.swing.JComponent;

/**
 * The application's {@code ActionManager} provides read-only cached
 * access to {@code ActionMaps} that contain one entry for each method
 * marked with the {@code @Action} annotation in a class.
 * 
 * 
 * @see ApplicationContext#getActionMap(Object)
 * @see ApplicationActionMap
 * @see ApplicationAction
 * @author Hans Muller (Hans.Muller@Sun.COM)
 */
public class ActionManager extends AbstractBean {

    private static final Logger logger = Logger.getLogger(ActionManager.class.getName());
    private final ApplicationContext context;
    private final WeakHashMap<Object, WeakReference<ApplicationActionMap>> actionMaps;
    private ApplicationActionMap globalActionMap = null;

    protected ActionManager(ApplicationContext context) {
        if (context == null) {
            throw new IllegalArgumentException("null context");
        }
        this.context = context;
        actionMaps = new WeakHashMap<Object, WeakReference<ApplicationActionMap>>();
    }

    protected final ApplicationContext getContext() {
        return context;
    }

    private ApplicationActionMap createActionMapChain(
            Class startClass, Class stopClass, Object actionsObject, ResourceMap resourceMap) {
        // All of the classes from stopClass to startClass, inclusive. 
        List<Class> classes = new ArrayList<Class>();
        for (Class c = startClass;; c = c.getSuperclass()) {
            classes.add(c);
            if (c.equals(stopClass)) {
                break;
            }
        }
        Collections.reverse(classes);
        // Create the ActionMap chain, one per class
        ApplicationContext ctx = getContext();
        ApplicationActionMap parent = null;
        for (Class cls : classes) {
            ApplicationActionMap appAM = new ApplicationActionMap(ctx, cls, actionsObject, resourceMap);
            appAM.setParent(parent);
            parent = appAM;
        }
        return parent;
    }

    /** 
     * The {@code ActionMap} chain for the entire {@code Application}.
     * <p>
     * Returns an {@code ActionMap} with the {@code @Actions} defined 
     * in the application's {@code Application} subclass, i.e. the
     * the value of: 
     * <pre>
     * ApplicationContext.getInstance().getApplicationClass()
     * </pre>
     * The remainder of the chain contains one {@code ActionMap}
     * for each superclass, up to {@code Application.class}.  The
     * {@code ActionMap.get()} method searches the entire chain, so
     * logically, the {@code ActionMap} that this method returns contains
     * all of the application-global actions.
     * <p>
     * The value returned by this method is cached.
     * 
     * @return the {@code ActionMap} chain for the entire {@code Application}.
     * @see #getActionMap(Class, Object)
     * @see ApplicationContext#getActionMap()
     * @see ApplicationContext#getActionMap(Class, Object)
     * @see ApplicationContext#getActionMap(Object)
     */
    public ApplicationActionMap getActionMap() {
        if (globalActionMap == null) {
            ApplicationContext ctx = getContext();
            Object appObject = ctx.getApplication();
            Class appClass = ctx.getApplicationClass();
            ResourceMap resourceMap = ctx.getResourceMap();
            globalActionMap = createActionMapChain(appClass, Application.class, appObject, resourceMap);
            initProxyActionSupport();  // lazy initialization
        }
        return globalActionMap;
    }

    private void initProxyActionSupport() {
        KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
        kfm.addPropertyChangeListener(new KeyboardFocusPCL());
    }

    /**
     * Returns the {@code ApplicationActionMap} chain for the specified
     * actions class and target object.
     * <p>
     * The specified class can contain methods marked with
     * the {@code @Action} annotation.  Each one will be turned
     * into an {@link ApplicationAction ApplicationAction} object
     * and all of them will be added to a single 
     * {@link ApplicationActionMap ApplicationActionMap}.  All of the 
     * {@code ApplicationActions} invoke their {@code actionPerformed}
     * method on the specified {@code actionsObject}.
     * The parent of the returned {@code ActionMap} is the global
     * {@code ActionMap} that contains the {@code @Actions} defined
     * in this application's {@code Application} subclass.
     * 
     * <p>
     * To bind an {@code @Action} to a Swing component, one specifies
     * the {@code @Action's} name in an expression like this:
     * <pre>
     * ApplicationContext ctx = Application.getInstance(MyApplication.class).getContext();
     * MyActions myActions = new MyActions();
     * myComponent.setAction(ac.getActionMap(myActions).get("myAction"));
     * </pre>
     * 
     * <p>
     * The value returned by this method is cached.  The lifetime of
     * the cached entry will be the same as the lifetime of the {@code
     * actionsObject} and the {@code ApplicationActionMap} and {@code
     * ApplicationActions} that refer to it.  In other words, if you
     * drop all references to the {@code actionsObject}, including
     * its {@code ApplicationActions} and their {@code
     * ApplicationActionMaps}, then the cached {@code ActionMap} entry
     * will be cleared.
     * 
     * @param actionsClass
     * @param actionsObject
     * @see #getActionMap()
     * @see ApplicationContext#getActionMap()
     * @see ApplicationContext#getActionMap(Class, Object)
     * @see ApplicationContext#getActionMap(Object)
     * @return the {@code ApplicationActionMap} for {@code actionsClass} and {@code actionsObject}
     */
    public ApplicationActionMap getActionMap(Class actionsClass, Object actionsObject) {
        if (actionsClass == null) {
            throw new IllegalArgumentException("null actionsClass");
        }
        if (actionsObject == null) {
            throw new IllegalArgumentException("null actionsObject");
        }
        if (!actionsClass.isAssignableFrom(actionsObject.getClass())) {
            throw new IllegalArgumentException("actionsObject not instanceof actionsClass");
        }
        synchronized (actionMaps) {
            WeakReference<ApplicationActionMap> ref = actionMaps.get(actionsObject);
            ApplicationActionMap classActionMap = (ref != null) ? ref.get() : null;
            if ((classActionMap == null) || (classActionMap.getActionsClass() != actionsClass)) {
                ApplicationContext ctx = getContext();
                Class actionsObjectClass = actionsObject.getClass();
                ResourceMap resourceMap = ctx.getResourceMap(actionsObjectClass, actionsClass);
                classActionMap = createActionMapChain(actionsObjectClass, actionsClass, actionsObject, resourceMap);
                ActionMap lastActionMap = classActionMap;
                while (lastActionMap.getParent() != null) {
                    lastActionMap = lastActionMap.getParent();
                }
                lastActionMap.setParent(getActionMap());
                actionMaps.put(actionsObject, new WeakReference<ApplicationActionMap>(classActionMap));
            }
            return classActionMap;
        }
    }

    private final class KeyboardFocusPCL implements PropertyChangeListener {

        private final TextActions textActions;

        KeyboardFocusPCL() {
            textActions = new TextActions(getContext());
        }

        @Override
        public void propertyChange(PropertyChangeEvent e) {
            if ("permanentFocusOwner".equals(e.getPropertyName())) {
                JComponent oldOwner = getContext().getFocusOwner();
                Object newValue = e.getNewValue();
                JComponent newOwner = (newValue instanceof JComponent) ? (JComponent) newValue : null;
                textActions.updateFocusOwner(oldOwner, newOwner);
                getContext().setFocusOwner(newOwner);
                updateAllProxyActions(oldOwner, newOwner);
            }
        }
    }

    /* For each proxyAction in each ApplicationActionMap, if
     * the newFocusOwner's ActionMap includes an Action with the same
     * name then bind the proxyAction to it, otherwise set the proxyAction's 
     * proxyBinding to null.  [TBD: synchronize access to actionMaps]
     */
    private void updateAllProxyActions(JComponent oldFocusOwner, JComponent newFocusOwner) {
        if (newFocusOwner != null) {
            ActionMap ownerActionMap = newFocusOwner.getActionMap();
            if (ownerActionMap != null) {
                updateProxyActions(getActionMap(), ownerActionMap, newFocusOwner);
                for (WeakReference<ApplicationActionMap> appAMRef : actionMaps.values()) {
                    ApplicationActionMap appAM = appAMRef.get();
                    if (appAM == null) {
                        continue;
                    }
                    updateProxyActions(appAM, ownerActionMap, newFocusOwner);
                }
            }
        }
    }

    /* For each proxyAction in appAM: if there's an action with the same
     * name in the focusOwner's ActionMap, then set the proxyAction's proxy
     * to the matching Action.  In other words: calls to the proxyAction
     * (actionPerformed) will delegate to the matching Action.
     */
    private void updateProxyActions(ApplicationActionMap appAM, ActionMap ownerActionMap, JComponent focusOwner) {
        for (ApplicationAction proxyAction : appAM.getProxyActions()) {
            String proxyActionName = proxyAction.getName();
            javax.swing.Action proxy = ownerActionMap.get(proxyActionName);
            if (proxy != null) {
                proxyAction.setProxy(proxy);
                proxyAction.setProxySource(focusOwner);
            } else {
                proxyAction.setProxy(null);
                proxyAction.setProxySource(null);
            }
        }
    }
}

