/*
 * $Id: SwingXUtilities.java,v 1.11 2009/04/02 20:35:54 kschaefe Exp $
 *
 * Copyright 2008 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;

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Font;
import java.awt.Frame;
import java.awt.Window;
import java.awt.event.InputEvent;
import java.io.IOException;
import java.io.StringReader;
import java.lang.reflect.Method;
import java.util.Locale;

import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;
import javax.swing.MenuElement;
import javax.swing.RepaintManager;
import javax.swing.SwingUtilities;
import javax.swing.plaf.ComponentInputMapUIResource;
import javax.swing.plaf.UIResource;
import javax.swing.text.html.HTMLDocument;

/**
 * A collection of utility methods for Swing(X) classes.
 * 
 * <ul>
 * PENDING JW: think about location of this class and/or its methods, Options:
 * 
 *  <li> move this class to the swingx utils package which already has a bunch of xxUtils
 *  <li> move methods between xxUtils classes as appropriate (one window/comp related util)
 *  <li> keep here in swingx (consistent with swingutilities in core)
 * </ul>
 * @author Karl George Schaefer
 */
public final class SwingXUtilities {
    private SwingXUtilities() {
        //does nothing
    }


    /**
     * A helper for creating and updating key bindings for components with
     * mnemonics. The {@code pressed} action will be invoked when the mnemonic
     * is activated.
     * <p>
     * TODO establish an interface for the mnemonic properties, such as {@code
     * MnemonicEnabled} and change signature to {@code public static <T extends
     * JComponent & MnemonicEnabled> void updateMnemonicBinding(T c, String
     * pressed)}
     * 
     * @param c
     *            the component bindings to update
     * @param pressed
     *            the name of the action in the action map to invoke when the
     *            mnemonic is pressed
     * @throws NullPointerException
     *             if the component is {@code null}
     */
    public static void updateMnemonicBinding(JComponent c, String pressed) {
        updateMnemonicBinding(c, pressed, null);
    }
    
    /**
     * A helper for creating and updating key bindings for components with
     * mnemonics. The {@code pressed} action will be invoked when the mnemonic
     * is activated and the {@code released} action will be invoked when the
     * mnemonic is deactivated.
     * <p>
     * TODO establish an interface for the mnemonic properties, such as {@code
     * MnemonicEnabled} and change signature to {@code public static <T extends
     * JComponent & MnemonicEnabled> void updateMnemonicBinding(T c, String
     * pressed, String released)}
     * 
     * @param c
     *            the component bindings to update
     * @param pressed
     *            the name of the action in the action map to invoke when the
     *            mnemonic is pressed
     * @param released
     *            the name of the action in the action map to invoke when the
     *            mnemonic is released (if the action is a toggle style, then
     *            this parameter should be {@code null})
     * @throws NullPointerException
     *             if the component is {@code null}
     */
    public static void updateMnemonicBinding(JComponent c, String pressed, String released) {
        Class<?> clazz = c.getClass();
        int m = -1;
        
        try {
            Method mtd = clazz.getMethod("getMnemonic");
            m = (Integer) mtd.invoke(c);
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new IllegalArgumentException("unable to access mnemonic", e);
        }
        
        InputMap map = SwingUtilities.getUIInputMap(c,
                JComponent.WHEN_IN_FOCUSED_WINDOW);
        
        if (m != 0) {
            if (map == null) {
                map = new ComponentInputMapUIResource(c);
                SwingUtilities.replaceUIInputMap(c,
                        JComponent.WHEN_IN_FOCUSED_WINDOW, map);
            }
            
            map.clear();
            
            //TODO is ALT_MASK right for all platforms?
            map.put(KeyStroke.getKeyStroke(m, InputEvent.ALT_MASK, false),
                    pressed);
            map.put(KeyStroke.getKeyStroke(m, InputEvent.ALT_MASK, true),
                    released);
            map.put(KeyStroke.getKeyStroke(m, 0, true), released);
        } else {
            if (map != null) {
                map.clear();
            }
        }
    }
    
    private static Component[] getChildren(Component c) {
        Component[] children = null;
        
        if (c instanceof MenuElement) {
            MenuElement[] elements = ((MenuElement) c).getSubElements();
            children = new Component[elements.length];
            
            for (int i = 0; i < elements.length; i++) {
                children[i] = elements[i].getComponent();
            }
        } else if (c instanceof Container) {
            children = ((Container) c).getComponents();
        }
        
        return children;
    }
    
    /**
     * Enables or disables of the components in the tree starting with {@code c}.
     * 
     * @param c
     *                the starting component
     * @param enabled
     *                {@code true} if the component is to enabled; {@code false} otherwise
     */
    public static void setComponentTreeEnabled(Component c, boolean enabled) {
        c.setEnabled(enabled);
        
        Component[] children = getChildren(c);
            
        if (children != null) {
            for(int i = 0; i < children.length; i++) {
                setComponentTreeEnabled(children[i], enabled);
            }
        }
    }
    
    /**
     * Sets the locale for an entire component hierarchy to the specified
     * locale.
     * 
     * @param c
     *                the starting component
     * @param locale
     *                the locale to set
     */
    public static void setComponentTreeLocale(Component c, Locale locale) {
        c.setLocale(locale);
        
        Component[] children = getChildren(c);
        
        if (children != null) {
            for(int i = 0; i < children.length; i++) {
                setComponentTreeLocale(children[i], locale);
            }
        }
    }

    /**
     * Sets the background for an entire component hierarchy to the specified
     * color.
     * 
     * @param c
     *                the starting component
     * @param color
     *                the color to set
     */
    public static void setComponentTreeBackground(Component c, Color color) {
        c.setBackground(color);
        
        Component[] children = getChildren(c);
        
        if (children != null) {
            for(int i = 0; i < children.length; i++) {
                setComponentTreeBackground(children[i], color);
            }
        }
    }

    /**
     * Sets the foreground for an entire component hierarchy to the specified
     * color.
     * 
     * @param c
     *                the starting component
     * @param color
     *                the color to set
     */
    public static void setComponentTreeForeground(Component c, Color color) {
        c.setForeground(color);
        
        Component[] children = getChildren(c);
        
        if (children != null) {
            for(int i = 0; i < children.length; i++) {
                setComponentTreeForeground(children[i], color);
            }
        }
    }

    /**
     * Sets the font for an entire component hierarchy to the specified font.
     * 
     * @param c
     *            the starting component
     * @param font
     *            the font to set
     */
    public static void setComponentTreeFont(Component c, Font font) {
        c.setFont(font);
        
        Component[] children = getChildren(c);
        
        if (children != null) {
            for(int i = 0; i < children.length; i++) {
                setComponentTreeFont(children[i], font);
            }
        }
    }

    private static String STYLESHEET = 
        "body { margin-top: 0; margin-bottom: 0; margin-left: 0; margin-right: 0;"
        + " font-family: %s; font-size: %dpt;  }"
        + "a, p, li { margin-top: 0; margin-bottom: 0; margin-left: 0;"
        + " margin-right: 0; font-family: %s; font-size: %dpt;  }";
    
    /**
     * Sets the font used for HTML displays to the specified font. Components
     * that display HTML do not necessarily honor font properties, since the
     * HTML document can override these values. Calling {@code setHtmlFont}
     * after the data is set will force the HTML display to use the font
     * specified to this method.
     * 
     * @param doc
     *            the HTML document to update
     * @param font
     *            the font to use
     * @throws NullPointerException
     *             if any parameter is {@code null}
     */
    public static void setHtmlFont(HTMLDocument doc, Font font) {
        String stylesheet = String.format(STYLESHEET, font.getName(),
                font.getSize(), font.getName(), font.getSize());
        
        try {
            doc.getStyleSheet().loadRules(new StringReader(stylesheet), null);
        } catch (IOException e) {
            //this should never happen with our sheet
            throw new IllegalStateException(e);
        }
    }
    
    /**
     * Updates the componentTreeUI of all top-level windows of the 
     * current application.
     * 
     */
    public static void updateAllComponentTreeUIs() {
        for (Frame frame : Frame.getFrames()) {
            updateAllComponentTreeUIs(frame);
        }
        
    }



    /**
     * Updates the componentTreeUI of the given window and all its
     * owned windows, recursively.
     * 
     * 
     * @param window the window to update
     */
    public static void updateAllComponentTreeUIs(Window window) {
        SwingUtilities.updateComponentTreeUI(window);
        for (Window owned : window.getOwnedWindows()) {
            updateAllComponentTreeUIs(owned);
        }
    }

    /**
     * An improved version of
     * {@link SwingUtilities#getAncestorOfClass(Class, Component)}. This method
     * traverses {@code JPopupMenu} invoker and uses generics to return an
     * appropriately typed object.
     * 
     * @param <T>
     *            the type of ancestor to find
     * @param clazz
     *            the class instance of the ancestor to find
     * @param c
     *            the component to start the search from
     * @return an ancestor of the correct type or {@code null} if no such
     *         ancestor exists. This method also returns {@code null} if any
     *         parameter is {@code null}.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getAncestor(Class<T> clazz, Component c) {
        if (clazz == null || c == null) {
            return null;
        }
        
        Component parent = c.getParent();

        while (parent != null && !(clazz.isInstance(parent))) {
            parent = c instanceof JPopupMenu
                    ? ((JPopupMenu) c).getInvoker() : c.getParent();
        }
        
        return (T) parent;
    }

    /**
     * Returns whether the component is part of the parent's
     * container hierarchy. If a parent in the chain is of type 
     * JPopupMenu, the parent chain of its invoker is walked.
     * 
     * @param focusOwner
     * @param parent
     * @return true if the component is contained under the parent's 
     *    hierarchy, coping with JPopupMenus.
     */
    public static boolean isDescendingFrom(Component focusOwner, Component parent) {
        while (focusOwner !=  null) {
            if (focusOwner instanceof JPopupMenu) {
                focusOwner = ((JPopupMenu) focusOwner).getInvoker();
                if (focusOwner == null) {
                    return false;
                }
            }
            if (focusOwner == parent) {
                return true;
            }
            focusOwner = focusOwner.getParent();
        }
        return false;
    }

    /**
     * Obtains a {@code TranslucentRepaintManager} from the specified manager.
     * If the current manager is a {@code TranslucentRepaintManager} or a
     * {@code ForwardingRepaintManager} that contains a {@code
     * TranslucentRepaintManager}, then the passed in manager is returned.
     * Otherwise a new repaint manager is created and returned.
     * 
     * @param delegate
     *            the current repaint manager
     * @return a non-{@code null} {@code TranslucentRepaintManager}
     * @throws NullPointerException if {@code delegate} is {@code null}
     */
    static RepaintManager getTranslucentRepaintManager(RepaintManager delegate) {
        RepaintManager manager = delegate;
        
        while (manager != null && !manager.getClass().isAnnotationPresent(TranslucentRepaintManager.class)) {
            if (manager instanceof ForwardingRepaintManager) {
                manager = ((ForwardingRepaintManager) manager).getDelegateManager();
            } else {
                manager = null;
            }
        }
        
        return manager == null ? new RepaintManagerX(delegate) : delegate;
    }
    
    /**
     * Checks and returns whether the given property should be replaced
     * by the UI's default value. 
     * 
     * @param property the property to check.
     * @return true if the given property should be replaced by the UI's
     *   default value, false otherwise. 
     */
    public static boolean isUIInstallable(Object property) {
       return (property == null) || (property instanceof UIResource);
    }

}
