/*
 * Created on May 31, 2005
 */
package org.flexdock.docking.floating.policy;

import java.util.Iterator;
import java.util.Map;
import java.util.Vector;

import org.flexdock.docking.Dockable;
import org.flexdock.docking.drag.DragManager;
import org.flexdock.docking.event.DockingEvent;
import org.flexdock.docking.event.DockingListener;

/**
 * This class provides centralized control over the framework's floating
 * behavior. This includes global behavior and behavior local to each individual
 * docking operation.
 * <p>
 * This class contains a method {@code isGlobalFloatingEnabled()} that indicates
 * whether global floating support is enabled. If global floating support is
 * disabled, then the setting governs all docking operations and blocks floating
 * in a global sense. If global floating support is enabled, then floating is
 * allowed or disallowed on an individual operation-by-operation basis through a
 * set of {@code FloatPolicy} implementations.
 * <p>
 * The default setting for global floating support is {@code false}. This,
 * however, may be controlled by the system property {@code FLOATING_ALLOWED}.
 * If the framework starts up with a case-sensitive {@code String} value of
 * {@code "true"} for this system property, then global floating support will be
 * turned on by default. Otherwise, global floating support may be modified via
 * {@code setGlobalFloatingEnabled(boolean globalFloatingEnabled)}.
 * <p>
 * This class provides methods {@code addPolicy(FloatPolicy policy)} and
 * {@code removePolicy(FloatPolicy policy)}, allowing the user to implement
 * custom behavior to control floating support for individual docking operations
 * on an event-by-event basis. By default, the {@code FloatPolicyManager} has a
 * single {@code FloatPolicy} installed of type {@code DefaultFloatPolicy}.
 * 
 * @author Christopher Butler
 */
public class FloatPolicyManager extends DockingListener.Stub {
    private static final FloatPolicyManager SINGLETON = new FloatPolicyManager();

    /**
     * Key constant used within the drag context {@code Map} to indicate whether
     * floating is allowed for a given drag operation.
     * 
     * @see DragManager#getDragContext(Dockable)
     */
    public static final String FLOATING_ALLOWED = "FloatPolicyManager.FLOATING_ALLOWED";

    /**
     * System property key used during framework initialization to determine the
     * default setting for global floating support.
     */
    public static final String GLOBAL_FLOATING_ENABLED = "global.floating.enabled";

    private Vector policies;

    private boolean globalFloatingEnabled;

    /**
     * Returns a singleton instance of the {@code FloatPolicyManager} class.
     * 
     * @return a singleton instance of the {@code FloatPolicyManager} class.
     */
    public static FloatPolicyManager getInstance() {
        return SINGLETON;
    }

    private FloatPolicyManager() {
        policies = new Vector();
        addPolicy(DefaultFloatPolicy.getInstance());
        globalFloatingEnabled = Boolean.getBoolean(GLOBAL_FLOATING_ENABLED);
    }

    /**
     * This method catches {@code DockingEvents} per the {@code DockingListener}
     * interface at the start of a drag operation and initializes floating
     * support within the the context {@code Map} of the drag operation. This
     * method retrieves the {@code Dockable} for the event via its
     * {@code getDockable()} method. It also retrieves the drag context
     * {@code Map} for the {@code DockingEvent} by invoking its
     * {@code getDragContext()} method. This map is the same {@code Map}
     * returned by {@code DragManager.getDragContext(Dockable dockable)}. It
     * then calls {@code isPolicyFloatingSupported(Dockable dockable)} for the
     * {@code Dockable} and places either {@code Boolean.TRUE} or
     * {@code Boolean.FALSE} within the drag context {@code Map}, caching the
     * value for use throughout the drag operation to avoid successive
     * iterations through the entire installed {@code FloatPolicy} collection.
     * The {@code Map}-key used is {@code FLOATING_ALLOWED}.
     * 
     * @param evt
     *            the {@code DockingEvent} whose drag context is to be
     *            initialized for floating support
     * @see DockingEvent#getDragContext()
     * @see DockingEvent#getDockable()
     * @see #isPolicyFloatingSupported(Dockable)
     * @see #FLOATING_ALLOWED
     */
    public void dragStarted(DockingEvent evt) {
        Map context = evt.getDragContext();
        Dockable d = evt.getDockable();
        Boolean allowed = isPolicyFloatingSupported(d) ? Boolean.TRUE
                : Boolean.FALSE;
        context.put(FLOATING_ALLOWED, allowed);
    }

    /**
     * This method catches {@code DockingEvents} per the {@code DockingListener}
     * interface at the end of a drag operation and determines whether or not to
     * block attempts to float within the docking operation.
     * <p>
     * If {@code evt.isOverWindow()} returns {@code true}, then the drop
     * operation is over an existing window and will be interpreted as an
     * attempt to dock within the window, not an attempt to float into a new
     * dialog. In this case, this method returns immediately with no action
     * taken.
     * <p>
     * This method calls {@code isFloatingAllowed(Dockable dockable)} using the
     * {@code DockingEvent's} {@code Dockable}, retrieved from
     * {@code getDockable()}. If this method returns {@code false}, then the
     * {@code DockingEvent} is consumed and this method returns.
     * <p>
     * If {@code isFloatingAllowed(Dockable dockable)} returns {@code true},
     * then the internal {@code FloatPolicy} collection is iterated through,
     * allowing each installed {@code FloatPolicy} to confirm the drop operation
     * via {@code isFloatDropAllowed(DockingEvent evt)}. If any of the
     * installed {@code FloatPolicies} returns {@code false} for
     * {@code isFloatDropAllowed(DockingEvent evt)}, then the
     * {@code DockingEvent} is consumed and the method exits.
     * <p>
     * If this method completes without the {@code DockingEvent} being consumed,
     * then the docking operation will proceed and attempts to float will be
     * allowed.
     * 
     * @param evt
     *            the {@code DockingEvent} to be examined for floating support
     * @see DockingEvent#isOverWindow()
     * @see DockingEvent#getDockable()
     * @see DockingEvent#consume()
     * @see #isFloatingAllowed(Dockable)
     * @see FloatPolicy#isFloatDropAllowed(DockingEvent)
     */
    public void dropStarted(DockingEvent evt) {
        if (evt.isOverWindow())
            return;

        if (!isFloatingAllowed(evt.getDockable())) {
            evt.consume();
            return;
        }

        for (Iterator it = policies.iterator(); it.hasNext();) {
            FloatPolicy policy = (FloatPolicy) it.next();
            if (!policy.isFloatDropAllowed(evt)) {
                evt.consume();
                return;
            }
        }
    }

    /**
     * Indicates whether floating is allowed for the specified {@code Dockable}.
     * If {@code dockable} is {@code null}, this method returns {@code false}.
     * <p>
     * This method first calls
     * {@code DragManager.getDragContext(Dockable dockable)} to see if a drag
     * operation is in progress. If so, it returns the {@code boolean} value
     * contained within the drag context map using the key
     * {@code FLOATING_ALLOWED}. If no mapping exists for the specified key,
     * this method returns {@code false}.
     * <p>
     * If no drag operation is currently in progress and no drag context can be
     * found, this method dispatches to
     * {@code isPolicyFloatingSupported(Dockable dockable)}, which iterates
     * through all installed {@code FloatPolicies} to determine whether floating
     * support is allowed.
     * 
     * @param dockable
     *            the {@code Dockable} whose floating support is to be checked
     * @return {@code true} if floating is allowed for the specified
     *         {@code Dockable}; {@code false} otherwise.
     * @see DragManager#getDragContext(Dockable)
     * @see #getInstance()
     * @see #isPolicyFloatingSupported(Dockable)
     * @see #FLOATING_ALLOWED
     */
    public static boolean isFloatingAllowed(Dockable dockable) {
        if (dockable == null)
            return false;

        Map context = DragManager.getDragContext(dockable);
        if (context == null)
            return getInstance().isPolicyFloatingSupported(dockable);

        Boolean floatAllowed = (Boolean) context.get(FLOATING_ALLOWED);
        return floatAllowed == null ? true : floatAllowed.booleanValue();
    }

    /**
     * Indicates whether floating is allowed for the specified {@code Dockable}
     * strictly by checking the installed {@code FloatPolicies}. If
     * {@code dockable} is {@code null}, this method returns {@code false}
     * immediately without checking through the installed {@code FloatPolicies}.
     * <p>
     * This method iterates through all installed {@code FloatPolicies} to
     * determine whether floating support is allowed. If any {@code FloatPolicy}
     * within the internal collection returns {@code false} from its
     * {@code isFloatingAllowed(Dockable dockable)} method, this method returns
     * {@code false}. Otherwise, this method returns {@code true}.
     * 
     * @param dockable
     *            the {@code Dockable} whose floating support is to be checked
     * @return {@code true} if floating is allowed for the specified
     *         {@code Dockable}; {@code false} otherwise.
     * @see FloatPolicy#isFloatingAllowed(Dockable)
     */
    public boolean isPolicyFloatingSupported(Dockable dockable) {
        if (dockable == null)
            return false;

        for (Iterator it = policies.iterator(); it.hasNext();) {
            FloatPolicy policy = (FloatPolicy) it.next();
            if (!policy.isFloatingAllowed(dockable))
                return false;
        }
        return true;
    }

    /**
     * Adds the specified {@code FloatPolicy} to the internal policy collection.
     * This {@code FloatPolicy} will now take part in framework determinations
     * as to whether floating should be supported during docking operations. If
     * {@code policy} is {@code null}, no action is taken.
     * 
     * @param policy
     *            the {@code FloatPolicy} to add to the system
     * @see #removePolicy(FloatPolicy)
     */
    public void addPolicy(FloatPolicy policy) {
        if (policy != null)
            policies.add(policy);
    }

    /**
     * Removes the specified {@code FloatPolicy} from the internal policy
     * collection. {@code FloatPolicy} will no longer take part in framework
     * determinations as to whether floating should be supported during docking
     * operations. If {@code policy} is {@code null} or was not previously
     * installed, no action is taken.
     * 
     * @param policy
     *            the {@code FloatPolicy} to remove from the system
     * @see #addPolicy(FloatPolicy)
     */
    public void removePolicy(FloatPolicy policy) {
        if (policy != null)
            policies.remove(policy);
    }

    /**
     * Returns a global setting used to control default framework floating
     * behavior. If this method returns {@code false}, all floating support for
     * the entire framework is turned off. If this method returns {@code true},
     * then floating support for individual docking operations is deferred to
     * the installed {@code FloatPolicies}.
     * 
     * @return {@code true} if global floating support is enabled; {@code false}
     *         otherwise.
     * @see #setGlobalFloatingEnabled(boolean)
     */
    public static boolean isGlobalFloatingEnabled() {
        return getInstance().globalFloatingEnabled;
    }

    /**
     * Sets the global setting used to control default framework floating
     * behavior. If {@code globalFloatingEnabled} is {@code false}, all
     * floating support for the entire framework is turned off. If
     * {@code globalFloatingEnabled} is {@code true}, then floating support for
     * individual docking operations is deferred to the installed
     * {@code FloatPolicies}.
     * 
     * @param globalFloatingEnabled
     *            {@code true} if global floating support is to be enabled;
     *            {@code false} otherwise.
     * @see #isGlobalFloatingEnabled()
     */
    public static void setGlobalFloatingEnabled(boolean globalFloatingEnabled) {
        getInstance().globalFloatingEnabled = globalFloatingEnabled;
    }
}
