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

import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Logger;
import javax.swing.JComponent;

/**
 * A singleton that manages shared objects, like actions, resources, and tasks, 
 * for {@code Applications}.  
 * <p>
 * {@link Application Applications} use {@code ApplicationContext},
 * via {@link Application#getContext}, to access global values and services.
 * The majority of the Swing Application Framework API can be accessed through {@code
 * ApplicationContext}.
 * 
 * @see Application
 * @author Hans Muller (Hans.Muller@Sun.COM)
 */
public class ApplicationContext extends AbstractBean {

    private static final Logger logger = Logger.getLogger(ApplicationContext.class.getName());
    private final List<TaskService> taskServices;
    private final List<TaskService> taskServicesReadOnly;
    private ResourceManager resourceManager;
    private ActionManager actionManager;
    private LocalStorage localStorage;
    private SessionStorage sessionStorage;
    private Application application = null;
    private Class applicationClass = null;
    private JComponent focusOwner = null;
    private Clipboard clipboard = null;
    private TaskMonitor taskMonitor = null;

    protected ApplicationContext() {
        resourceManager = new ResourceManager(this);
        actionManager = new ActionManager(this);
        localStorage = new LocalStorage(this);
        sessionStorage = new SessionStorage(this);
        taskServices = new CopyOnWriteArrayList<TaskService>();
        taskServices.add(new TaskService(TaskService.DEFAULT_NAME));
        taskServicesReadOnly = Collections.unmodifiableList(taskServices);
    }

    /**
     * Returns the application's class or null if the application
     * hasn't been launched and this property hasn't been set.  Once
     * the application has been launched, the value returned by this
     * method is the same as {@code getApplication().getClass()}.
     * 
     * @return the application's class or null
     * @see #setApplicationClass
     * @see #getApplication
     */
    public final synchronized Class getApplicationClass() {
        return applicationClass;
    }

    /**
     * Called by 
     * {@link Application#launch Application.launch()} to 
     * record the application's class.
     * <p>
     * This method is only intended for testing, or design time
     * configuration.  Normal applications shouldn't need to 
     * call it directly.
     *
     * @param applicationClass
     * @see #getApplicationClass
     */
    public final synchronized void setApplicationClass(Class applicationClass) {
        if (this.application != null) {
            throw new IllegalStateException("application has been launched");
        }
        this.applicationClass = applicationClass;
    }

    /**
     * The {@code Application} singleton, or null if {@code launch} hasn't
     * been called yet.
     * 
     * @return the launched Application singleton.
     * @see Application#launch
     */
    public final synchronized Application getApplication() {
        return application;
    }

    /* Called by Application.launch().
     */
    synchronized void setApplication(Application application) {
        if (this.application != null) {
            throw new IllegalStateException("application has already been launched");
        }
        this.application = application;
    }

    /**
     * The application's {@code ResourceManager} provides 
     * read-only cached access to resources in ResourceBundles via the 
     * {@link ResourceMap ResourceMap} class.
     * 
     * @return this application's ResourceManager.
     * @see #getResourceMap(Class, Class)
     */
    public final ResourceManager getResourceManager() {
        return resourceManager;
    }

    /**
     * Change this application's {@code ResourceManager}.  An
     * {@code ApplicationContext} subclass that
     * wanted to fundamentally change the way {@code ResourceMaps} were
     * created and cached could replace this property in its constructor.
     * <p>
     * Throws an IllegalArgumentException if resourceManager is null.
     * 
     * @param resourceManager the new value of the resourceManager property. 
     * @see #getResourceMap(Class, Class)
     * @see #getResourceManager
     */
    protected void setResourceManager(ResourceManager resourceManager) {
        if (resourceManager == null) {
            throw new IllegalArgumentException("null resourceManager");
        }
        Object oldValue = this.resourceManager;
        this.resourceManager = resourceManager;
        firePropertyChange("resourceManager", oldValue, this.resourceManager);
    }

    /**
     * Returns a {@link ResourceMap#getParent chain} of two or
     * more ResourceMaps.  The first encapsulates the ResourceBundles
     * defined for the specified class, and its parent 
     * encapsulates the ResourceBundles defined for the entire application.
     * <p>
     *  This is just a convenience method that calls
     * {@link ResourceManager#getResourceMap(Class, Class)
     * ResourceManager.getResourceMap()}.  It's defined as:
     * <pre>
     * return getResourceManager().getResourceMap(cls, cls);
     * </pre>
     * 
     * @param cls the class that defines the location of ResourceBundles
     * @return a {@code ResourceMap} that contains resources loaded from 
     *   {@code ResourceBundles}  found in the resources subpackage of the 
     *   specified class's package.
     * @see ResourceManager#getResourceMap(Class)
     */
    public final ResourceMap getResourceMap(Class cls) {
        return getResourceManager().getResourceMap(cls, cls);
    }

    /**
     * Returns a {@link ResourceMap#getParent chain} of two or more
     * ResourceMaps.  The first encapsulates the ResourceBundles
     * defined for the all of the classes between {@code startClass}
     * and {@code stopClass} inclusive.  It's parent encapsulates the
     * ResourceBundles defined for the entire application.
     * <p>
     *  This is just a convenience method that calls
     * {@link ResourceManager#getResourceMap(Class, Class)
     * ResourceManager.getResourceMap()}.  It's defined as:
     * <pre>
     * return getResourceManager().getResourceMap(startClass, stopClass);
     * </pre>
     * 
     * @param startClass the first class whose ResourceBundles will be included
     * @param stopClass the last class whose ResourceBundles will be included
     * @return a {@code ResourceMap} that contains resources loaded from 
     *   {@code ResourceBundles}  found in the resources subpackage of the 
     *   specified class's package.
     * @see ResourceManager#getResourceMap(Class, Class)
     */
    public final ResourceMap getResourceMap(Class startClass, Class stopClass) {
        return getResourceManager().getResourceMap(startClass, stopClass);
    }

    /**
     * Returns the {@link ResourceMap#getParent chain} of ResourceMaps 
     * that's shared by the entire application, beginning with the one 
     * defined for the Application class, i.e. the value of the 
     * {@code applicationClass} property.
     * <p>
     * This is just a convenience method that calls
     * {@link ResourceManager#getResourceMap()
     * ResourceManager.getResourceMap()}.  It's defined as:
     * <pre>
     * return getResourceManager().getResourceMap();
     * </pre>
     * 
     * @return the Application's ResourceMap
     * @see ResourceManager#getResourceMap()
     * @see #getApplicationClass
     */
    public final ResourceMap getResourceMap() {
        return getResourceManager().getResourceMap();
    }

    /**
     * Return this application's ActionManager.
     * @return this application's ActionManager.
     * @see #getActionMap(Object)
     */
    public final ActionManager getActionManager() {
        return actionManager;
    }

    /**
     * Change this application's {@code ActionManager}.  An
     * {@code ApplicationContext} subclass that
     * wanted to fundamentally change the way {@code ActionManagers} were
     * created and cached could replace this property in its constructor.
     * <p>
     * Throws an IllegalArgumentException if actionManager is null.
     * 
     * @param actionManager the new value of the actionManager property. 
     * @see #getActionManager
     * @see #getActionMap(Object)
     */
    protected void setActionManager(ActionManager actionManager) {
        if (actionManager == null) {
            throw new IllegalArgumentException("null actionManager");
        }
        Object oldValue = this.actionManager;
        this.actionManager = actionManager;
        firePropertyChange("actionManager", oldValue, this.actionManager);
    }

    /** 
     * Returns the shared {@code ActionMap} chain for the entire {@code Application}.
     * <p>
     *  This is just a convenience method that calls
     * {@link ActionManager#getActionMap()
     * ActionManager.getActionMap()}.  It's defined as:
     * <pre>
     * return getActionManager().getActionMap()
     * </pre>
     * 
     * @return the {@code ActionMap} chain for the entire {@code Application}.
     * @see ActionManager#getActionMap()
     */
    public final ApplicationActionMap getActionMap() {
        return getActionManager().getActionMap();
    }

    /** 
     * Returns the {@code ApplicationActionMap} chain for the specified
     * actions class and target object.
     * <p>
     *  This is just a convenience method that calls
     * {@link ActionManager#getActionMap()
     * ActionManager.getActionMap(Class, Object)}.  It's defined as:
     * <pre>
     * return getActionManager().getActionMap(actionsClass, actionsObject)
     * </pre>
     * 
     * @param actionsClass
     * @param actionsObject
     * @return the {@code ActionMap} chain for the entire {@code Application}.
     * @see ActionManager#getActionMap(Class, Object)
     */
    public final ApplicationActionMap getActionMap(Class actionsClass, Object actionsObject) {
        return getActionManager().getActionMap(actionsClass, actionsObject);
    }

    /** 
     * Defined as {@code getActionMap(actionsObject.getClass(), actionsObject)}.
     * 
     * @param actionsObject
     * @return the {@code ActionMap} for the specified object
     * @see #getActionMap(Class, Object)
     */
    public final ApplicationActionMap getActionMap(Object actionsObject) {
        if (actionsObject == null) {
            throw new IllegalArgumentException("null actionsObject");
        }
        return getActionManager().getActionMap(actionsObject.getClass(), actionsObject);
    }

    /**
     * The shared {@link LocalStorage LocalStorage} object.
     * 
     * @return the shared {@link LocalStorage LocalStorage} object.
     */
    public final LocalStorage getLocalStorage() {
        return localStorage;
    }

    /**
     * The shared {@link LocalStorage LocalStorage} object.
     * 
     * @param localStorage the shared {@link LocalStorage LocalStorage} object.
     */
    protected void setLocalStorage(LocalStorage localStorage) {
        if (localStorage == null) {
            throw new IllegalArgumentException("null localStorage");
        }
        Object oldValue = this.localStorage;
        this.localStorage = localStorage;
        firePropertyChange("localStorage", oldValue, this.localStorage);
    }

    /**
     * The shared {@link SessionStorage SessionStorage} object.
     * 
     * @return the shared {@link SessionStorage SessionStorage} object.
     */
    public final SessionStorage getSessionStorage() {
        return sessionStorage;
    }

    /**
     * The shared {@link SessionStorage SessionStorage} object.
     * 
     * @param sessionStorage the shared {@link SessionStorage SessionStorage} object.
     */
    protected void setSessionStorage(SessionStorage sessionStorage) {
        if (sessionStorage == null) {
            throw new IllegalArgumentException("null sessionStorage");
        }
        Object oldValue = this.sessionStorage;
        this.sessionStorage = sessionStorage;
        firePropertyChange("sessionStorage", oldValue, this.sessionStorage);
    }

    /**
     * Return a shared {@code Clipboard}.
     * @return A shared {@code Clipboard}.
     */
    public Clipboard getClipboard() {
        if (clipboard == null) {
            try {
                clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
            } catch (SecurityException e) {
                clipboard = new Clipboard("sandbox");
            }
        }
        return clipboard;
    }

    /**
     * Returns the application's focus owner.
     * @return  The application's focus owner.
     */
    public JComponent getFocusOwner() {
        return focusOwner;
    }

    /**
     * Changes the application's focus owner.
     * @param focusOwner new focus owner
     */
    void setFocusOwner(JComponent focusOwner) {
        Object oldValue = this.focusOwner;
        this.focusOwner = focusOwner;
        firePropertyChange("focusOwner", oldValue, this.focusOwner);
    }

    private List<TaskService> copyTaskServices() {
        return new ArrayList<TaskService>(taskServices);
    }

    /**
     * Register a new TaskService with the application. The task service
     * then be retrieved by name via {@link ApplicationContext#getTaskService(String)}.
     * 
     * @param taskService Task service to register
     */
    public void addTaskService(TaskService taskService) {
        if (taskService == null) {
            throw new IllegalArgumentException("null taskService");
        }
        List<TaskService> oldValue = null, newValue = null;
        boolean changed = false;
        synchronized (taskServices) {
            if (!taskServices.contains(taskService)) {
                oldValue = copyTaskServices();
                taskServices.add(taskService);
                newValue = copyTaskServices();
                changed = true;
            }
        }
        if (changed) {
            firePropertyChange("taskServices", oldValue, newValue);
        }
    }

    /**
     * Unregister a previously registered TaskService. The task service
     * is not shut down.
     *
     * @param taskService TaskService to unregister
     */
    public void removeTaskService(TaskService taskService) {
        if (taskService == null) {
            throw new IllegalArgumentException("null taskService");
        }
        List<TaskService> oldValue = null, newValue = null;
        boolean changed = false;
        synchronized (taskServices) {
            if (taskServices.contains(taskService)) {
                oldValue = copyTaskServices();
                taskServices.remove(taskService);
                newValue = copyTaskServices();
                changed = true;
            }
        }
        if (changed) {
            firePropertyChange("taskServices", oldValue, newValue);
        }
    }

    /**
     * Look up a task service by name.
     *
     * @param name Name of the task service to retrieve.
     * @return Task service found, or null if no service of that name found
     */
    public TaskService getTaskService(String name) {
        if (name == null) {
            throw new IllegalArgumentException("null name");
        }
        for (TaskService taskService : taskServices) {
            if (name.equals(taskService.getName())) {
                return taskService;
            }
        }
        return null;
    }

    /**
     * Returns the default TaskService, i.e. the one named "default":
     * <code>return getTaskService("default")</code>.  The 
     * {@link ApplicationAction#actionPerformed ApplicationAction actionPerformed}
     * method executes background <code>Tasks</code> on the default
     * TaskService.  Application's can launch Tasks in the same way, e.g.
     * <pre>
     * Application.getInstance().getContext().getTaskService().execute(myTask);
     * </pre>
     * 
     * @return the default TaskService.
     * @see #getTaskService(String)
     * 
     */
    public final TaskService getTaskService() {
        return getTaskService(TaskService.DEFAULT_NAME);
    }

    /**
     * Returns a read-only view of the complete list of TaskServices.
     * 
     * @return a list of all of the TaskServices.
     * @see #addTaskService
     * @see #removeTaskService
     */
    public List<TaskService> getTaskServices() {
        return taskServicesReadOnly;
    }

    /**
     * Returns a shared TaskMonitor object.  Most applications only 
     * need one TaskMonitor for the sake of status bars and other status
     * indicators.
     * 
     * @return the shared TaskMonitor object.
     */
    public final TaskMonitor getTaskMonitor() {
        if (taskMonitor == null) {
            taskMonitor = new TaskMonitor(this);
        }
        return taskMonitor;
    }
}
