/*
 * $Id: UIManagerExt.java,v 1.9 2008/02/26 04:31:50 kschaefe Exp $
 *
 * Copyright 2007 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.plaf;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Insets;
import java.awt.Shape;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Vector;

import javax.swing.Icon;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.plaf.BorderUIResource;
import javax.swing.plaf.ColorUIResource;
import javax.swing.plaf.DimensionUIResource;
import javax.swing.plaf.FontUIResource;
import javax.swing.plaf.IconUIResource;
import javax.swing.plaf.InsetsUIResource;
import javax.swing.plaf.UIResource;

import org.jdesktop.swingx.painter.Painter;
import org.jdesktop.swingx.util.Contract;

/**
 * A utility class for obtaining configuration properties from the
 * {@code UIDefaults}. This class handles SwingX-specific L&F needs, such as
 * the installation of painters and shapes. There are several categories of
 * utility methods:
 * <ul>
 * <li>Support for the safe creation of {@code UIResource}s.</li>
 * <li>Support for new {@code UIResource} types, such as
 * {@code PainterUIResource}.</li>
 * <li>Support for the dynamic localization of {@code UIDefaults}.</li>
 * <li>Support for returning non-{@code String} localizations from
 * {@code ResourceBundle}s.</li>
 * </ul>
 * <h3>Safe Methods</h3>
 * <p>
 * The {@code getSafeXXX} methods are designed for use with
 * {@code LookAndFeelAddon}s. Any addon that attempts to obtain a property
 * defined in the defaults (available from {@code UIManager.get}) to set a
 * property that will be added to the defaults for the addon should use the
 * "safe" methods. The methods ensure that a valid value is always returned and
 * that value is a {@code UIResource}.
 * </p>
 * <h3>Support for New Types</h3>
 * <p>
 * {@code UIManagerExt} supports the retrieval of new {@code UIResource} types.
 * There is a {@code getXXX} method for every {@code UIResource} subtype in the
 * {@code org.jdesktop.swingx.plaf} package.
 * </p>
 * <h3>Support for Dynamic Localization</h3>
 * <p>
 * {@code UIManagerExt} enables dynamic localization by supporting
 * {@code ResourceBundle}s. The
 * {@linkplain UIDefaults#addResourceBundle(String)} allows resource bundles to
 * be added to the {@code UIDefaults}. While there is support for this feature
 * in core, there is a bug with the class loader that prevents user added
 * bundles from working correctly when used via Web Start. Therefore,
 * {@code UIManagerExt} defines methods to add and remove resource bundles.
 * These are the only methods that SwingX classes should use when adding
 * resource bundles to the defaults. Since {@code UIManagerExt} is maintaining
 * the bundles, any localized {@code String}s <b>must</b> be retrieved from
 * the {@code getString} methods in this class.
 * </p>
 * <h3>Support for Non-{@code String} Localization Values</h3>
 * <p>
 * All methods work by first determining if the value is present
 * {@code UIDefaults}. If the value is not present, then the installed
 * {@code ResourceBundle}s are queried. {@code UIManagerExt} will attempt to
 * convert any returned value to the appropriate type. For instance,
 * {@code getInt} uses {@code Integer.decode} to convert {@code String}s
 * returned from the bundle into {@code int}s.
 * </p>
 * 
 * @author Karl George Schaefer
 * 
 * @see UIManager
 * @see UIDefaults
 */
public class UIManagerExt {
    /**
     * Used to replicate the resource bundle behavior from the
     * {@code UIDefaults}.
     */
    private static class UIDefaultsExt {
        //use vector; we want synchronization
        private Vector<String> resourceBundles;

        /**
         * Maps from a Locale to a cached Map of the ResourceBundle. This is done
         * so as to avoid an exception being thrown when a value is asked for.
         * Access to this should be done while holding a lock on the
         * UIDefaults, eg synchronized(this).
         */
        private Map<Locale, Map<String, String>> resourceCache;
        
        UIDefaultsExt() {
            resourceCache = new HashMap<Locale, Map<String,String>>();
        }
        
        //should this just return String?
        private Object getFromResourceBundle(Object key, Locale l) {

            if( resourceBundles == null ||
                resourceBundles.isEmpty() ||
                !(key instanceof String) ) {
                return null;
            }

            // A null locale means use the default locale.
            if( l == null ) {
                    l = Locale.getDefault();
            }

            synchronized(this) {
                return getResourceCache(l).get((String)key);
            }
        }

        /**
         * Returns a Map of the known resources for the given locale.
         */
        private Map<String, String> getResourceCache(Locale l) {
            Map<String, String> values = (Map<String, String>) resourceCache.get(l);

            if (values == null) {
                values = new HashMap<String, String>();
                for (int i=resourceBundles.size()-1; i >= 0; i--) {
                    String bundleName = (String)resourceBundles.get(i);
                    
                    try {
                        ResourceBundle b = ResourceBundle.
                            getBundle(bundleName, l, UIManagerExt.class.getClassLoader());
                        Enumeration<String> keys = b.getKeys();

                        while (keys.hasMoreElements()) {
                            String key = (String)keys.nextElement();

                            if (values.get(key) == null) {
                                Object value = b.getObject(key);

                                values.put(key, (String) value);
                            }
                        }
                    } catch( MissingResourceException mre ) {
                        // Keep looking
                    }
                }
                resourceCache.put(l, values);
            }
            return values;
        }

        public synchronized void addResourceBundle(String bundleName) {
            if( bundleName == null ) {
                return;
            }
            if( resourceBundles == null ) {
                resourceBundles = new Vector<String>(5);
            }
            if (!resourceBundles.contains(bundleName)) {
                resourceBundles.add( bundleName );
                resourceCache.clear();
            }
        }
        
        public synchronized void removeResourceBundle( String bundleName ) {
            if( resourceBundles != null ) {
                resourceBundles.remove( bundleName );
            }
            resourceCache.clear();
        }
    }
    
    private static UIDefaultsExt uiDefaultsExt = new UIDefaultsExt();
    
    private UIManagerExt() {
        //does nothing
    }
    
    /**
     * Adds a resource bundle to the list of resource bundles that are searched
     * for localized values. Resource bundles are searched in the reverse order
     * they were added. In other words, the most recently added bundle is
     * searched first.
     * 
     * @param bundleName
     *                the base name of the resource bundle to be added
     * @see java.util.ResourceBundle
     * @see #removeResourceBundle
     */
    public static void addResourceBundle(String bundleName) {
        uiDefaultsExt.addResourceBundle(bundleName);
    }
    
    /**
     * Removes a resource bundle from the list of resource bundles that are
     * searched for localized defaults.
     * 
     * @param bundleName
     *                the base name of the resource bundle to be removed
     * @see java.util.ResourceBundle
     * @see #addResourceBundle
     */
    public static void removeResourceBundle(String bundleName) {
        uiDefaultsExt.removeResourceBundle(bundleName);
    }
    
    /**
     * Returns a string from the defaults. If the value for {@code key} is not a
     * {@code String}, {@code null} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the string
     * @return the {@code String} object
     * @throws NullPointerException
     *                 if {@code key} is {@code null}
     */
    public static String getString(Object key) {
        return getString(key, null);
    }
    
    /**
     * Returns a string from the defaults. If the value for {@code key} is not a
     * {@code String}, {@code null} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the string
     * @param l
     *                the {@code Locale} for which the painter is desired; refer
     *                to {@code UIDefaults} for details on how a {@code null}
     *                {@code Locale} is handled
     * @return the {@code String} object
     * @throws NullPointerException
     *                 if {@code key} is {@code null}
     */
    public static String getString(Object key, Locale l) {
        Object value = UIManager.get(key, l);
        
        if (value instanceof String) {
            return (String) value;
        }
        
        //only return resource bundle if not in UIDefaults
        if (value == null) {
            value = uiDefaultsExt.getFromResourceBundle(key, l);
            
            if (value instanceof String) {
                return (String) value;
            }
        }
        
        return null;
    }
    
    /**
     * Returns an integer from the defaults. If the value for {@code key} is not
     * an {@code int}, {@code 0} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the integer
     * @return the {@code int}
     * @throws NullPointerException
     *                 if {@code key} is {@code null}
     */
    public static int getInt(Object key) {
        return getInt(key, null);
    }
    
    /**
     * Returns an integer from the defaults. If the value for {@code key} is not
     * an {@code int}, {@code 0} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the integer
     * @param l
     *                the {@code Locale} for which the integer is desired; refer
     *                to {@code UIDefaults} for details on how a {@code null}
     *                {@code Locale} is handled
     * @return the {@code int}
     * @throws NullPointerException
     *                 if {@code key} is {@code null}
     */
    public static int getInt(Object key, Locale l) {
        Object value = UIManager.get(key, l);
        
        if (value instanceof Integer) {
            return (Integer) value;
        }
        
        if (value == null) {
            value = uiDefaultsExt.getFromResourceBundle(key, l);
            
            if (value instanceof Integer) {
                return (Integer) value;
            }
            
            if (value instanceof String) {
                try {
                    return Integer.decode((String) value);
                } catch (NumberFormatException e) {
                    // ignore - the entry was not parseable, can't do anything
                    // JW: should we log it?
                }
            }
        }
        
        return 0;
    }
    
    /**
     * Returns an Boolean from the defaults. If the value for {@code key} is not
     * a {@code boolean}, {@code false} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the Boolean
     * @return the {@code boolean}
     * @throws NullPointerException
     *                 if {@code key} is {@code null}
     */
    public static boolean getBoolean(Object key) {
        return getBoolean(key, null);
    }
    
    /**
     * Returns an Boolean from the defaults. If the value for {@code key} is not
     * a {@code boolean}, {@code false} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the Boolean
     * @param l
     *                the {@code Locale} for which the Boolean is desired; refer
     *                to {@code UIDefaults} for details on how a {@code null}
     *                {@code Locale} is handled
     * @return the {@code boolean}
     * @throws NullPointerException
     *                 if {@code key} is {@code null}
     */
    public static boolean getBoolean(Object key, Locale l) {
        Object value = UIManager.get(key, l);
        
        if (value instanceof Boolean) {
            return (Boolean) value;
        }
        
        //only return resource bundle if not in UIDefaults
        if (value == null) {
            value = uiDefaultsExt.getFromResourceBundle(key, l);
            
            if (value instanceof Boolean) {
                return (Boolean) value;
            }
            
            if (value instanceof String) {
                return Boolean.valueOf((String) value);
            }
        }
        
        return false;
    }
    
    /**
     * Returns a color from the defaults. If the value for {@code key} is not
     * a {@code Color}, {@code null} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the color
     * @return the {@code Color} object
     * @throws NullPointerException
     *                 if {@code key} is {@code null}
     */
    public static Color getColor(Object key) {
        return getColor(key, null);
    }
    
    /**
     * Returns a color from the defaults. If the value for {@code key} is not
     * a {@code Color}, {@code null} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the color
     * @param l
     *                the {@code Locale} for which the color is desired; refer
     *                to {@code UIDefaults} for details on how a {@code null}
     *                {@code Locale} is handled
     * @return the {@code Color} object
     * @throws NullPointerException
     *                 if {@code key} is {@code null}
     */
    public static Color getColor(Object key, Locale l) {
        Object value = UIManager.get(key, l);
        
        if (value instanceof Color) {
            return (Color) value;
        }
        
        //only return resource bundle if not in UIDefaults
        if (value == null) {
            value = uiDefaultsExt.getFromResourceBundle(key, l);
            
            if (value instanceof Color) {
                return (Color) value;
            }
            
            if (value instanceof String) {
                try {
                    return Color.decode((String) value);
                } catch (NumberFormatException e) {
                    // incorrect format; does nothing
                }
            }
        }
        
        return null;
    }

    //TODO: Font.decode always returns a valid font.  This is not acceptable for UIManager
//    /**
//     * Returns a font from the defaults. If the value for {@code key} is not
//     * a {@code Font}, {@code null} is returned.
//     * 
//     * @param key
//     *                an {@code Object} specifying the font
//     * @return the {@code Font} object
//     * @throws NullPointerException
//     *                 if {@code key} is {@code null}
//     */
//    public static Font getFont(Object key) {
//        return getFont(key, null);
//    }
//    
//    /**
//     * Returns a font from the defaults. If the value for {@code key} is not
//     * a {@code Font}, {@code null} is returned.
//     * 
//     * @param key
//     *                an {@code Object} specifying the font
//     * @param l
//     *                the {@code Locale} for which the font is desired; refer
//     *                to {@code UIDefaults} for details on how a {@code null}
//     *                {@code Locale} is handled
//     * @return the {@code Font} object
//     * @throws NullPointerException
//     *                 if {@code key} is {@code null}
//     */
//    public static Font getFont(Object key, Locale l) {
//        Object value = UIManager.get(key, l);
//        
//        if (value instanceof Font) {
//            return (Font) value;
//        }
//        
//        //only return resource bundle if not in UIDefaults
//        if (value == null) {
//            value = uiDefaultsExt.getFromResourceBundle(key, l);
//            
//            if (value instanceof Font) {
//                return (Font) value;
//            }
//            
//            if (value instanceof String) {
//                return Font.decode((String) value);
//            }
//        }
//        
//        return null;
//    }
    
    /**
     * Returns a shape from the defaults. If the value for {@code key} is not a
     * {@code Shape}, {@code null} is returned.
     * 
     * @param key an {@code Object} specifying the shape
     * @return the {@code Shape} object
     * @throws NullPointerException if {@code key} is {@code null}
     */
    public static Shape getShape(Object key) {
        Object value = UIManager.getDefaults().get(key);
        return (value instanceof Shape) ? (Shape) value : null;
    }
    
    /**
     * Returns a shape from the defaults that is appropriate for the given
     * locale. If the value for {@code key} is not a {@code Shape},
     * {@code null} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the shape
     * @param l
     *                the {@code Locale} for which the shape is desired; refer
     *                to {@code UIDefaults} for details on how a {@code null}
     *                {@code Locale} is handled
     * @return the {@code Shape} object
     * @throws NullPointerException
     *                 if {@code key} is {@code null}
     */
    public static Shape getShape(Object key, Locale l) {
        Object value = UIManager.getDefaults().get(key, l);
        return (value instanceof Shape) ? (Shape) value : null;
    }
    
    /**
     * Returns a painter from the defaults. If the value for {@code key} is not
     * a {@code Painter}, {@code null} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the painter
     * @return the {@code Painter} object
     * @throws NullPointerException
     *                 if {@code key} is {@code null}
     */
    public static Painter<?> getPainter(Object key) {
        Object value = UIManager.getDefaults().get(key);
        return (value instanceof Painter) ? (Painter<?>) value : null;
    }
    
    /**
     * Returns a painter from the defaults that is appropriate for the given
     * locale. If the value for {@code key} is not a {@code Painter},
     * {@code null} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the painter
     * @param l
     *                the {@code Locale} for which the painter is desired; refer
     *                to {@code UIDefaults} for details on how a {@code null}
     *                {@code Locale} is handled
     * @return the {@code Painter} object
     * @throws NullPointerException
     *                 if {@code key} is {@code null}
     */
    public static Painter<?> getPainter(Object key, Locale l) {
        Object value = UIManager.getDefaults().get(key, l);
        return (value instanceof Painter) ? (Painter<?>) value : null;
    }
    
    /**
     * Returns a border from the defaults. If the value for {@code key} is not a
     * {@code Border}, {@code defaultBorder} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the border
     * @param defaultBorder
     *                the border to return if the border specified by
     *                {@code key} does not exist
     * @return the {@code Border} object
     * @throws NullPointerException
     *                 if {@code key} or {@code defaultBorder} is {@code null}
     */
    public static Border getSafeBorder(Object key, Border defaultBorder) {
        Contract.asNotNull(defaultBorder, "defaultBorder cannot be null");
        
        Border safeBorder = UIManager.getBorder(key);
        
        if (safeBorder == null) {
            safeBorder = defaultBorder;
        }
        
        if (!(safeBorder instanceof UIResource)) {
            safeBorder = new BorderUIResource(safeBorder);
        }
        
        return safeBorder;
    }
    
    /**
     * Returns a color from the defaults. If the value for {@code key} is not a
     * {@code Color}, {@code defaultColor} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the color
     * @param defaultColor
     *                the color to return if the color specified by {@code key}
     *                does not exist
     * @return the {@code Color} object
     * @throws NullPointerException
     *                 if {@code key} or {@code defaultColor} is {@code null}
     */
    public static Color getSafeColor(Object key, Color defaultColor) {
        Contract.asNotNull(defaultColor, "defaultColor cannot be null");
        
        Color safeColor = UIManager.getColor(key);
        
        if (safeColor == null) {
            safeColor = defaultColor;
        }
        
        if (!(safeColor instanceof UIResource)) {
            safeColor = new ColorUIResource(safeColor);
        }
        
        return safeColor;
    }
    
    /**
     * Returns a dimension from the defaults. If the value for {@code key} is
     * not a {@code Dimension}, {@code defaultDimension} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the dimension
     * @param defaultDimension
     *                the dimension to return if the dimension specified by
     *                {@code key} does not exist
     * @return the {@code Dimension} object
     * @throws NullPointerException
     *                 if {@code key} or {@code defaultColor} is {@code null}
     */
    public static Dimension getSafeDimension(Object key, Dimension defaultDimension) {
        Contract.asNotNull(defaultDimension, "defaultDimension cannot be null");
        
        Dimension safeDimension = UIManager.getDimension(key);
        
        if (safeDimension == null) {
            safeDimension = defaultDimension;
        }
        
        if (!(safeDimension instanceof UIResource)) {
            safeDimension = new DimensionUIResource(safeDimension.width, safeDimension.height);
        }
        
        return safeDimension;
    }
    
    /**
     * Returns a font from the defaults. If the value for {@code key} is not a
     * {@code Font}, {@code defaultFont} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the font
     * @param defaultFont
     *                the font to return if the font specified by {@code key}
     *                does not exist
     * @return the {@code Font} object
     * @throws NullPointerException
     *                 if {@code key} or {@code defaultFont} is {@code null}
     */
    public static Font getSafeFont(Object key, Font defaultFont) {
        Contract.asNotNull(defaultFont, "defaultFont cannot be null");
        
        Font safeFont = UIManager.getFont(key);
        
        if (safeFont == null) {
            safeFont = defaultFont;
        }
        
        if (!(safeFont instanceof UIResource)) {
            safeFont = new FontUIResource(safeFont);
        }
        
        return safeFont;
    }
    
    /**
     * Returns an icon from the defaults. If the value for {@code key} is not a
     * {@code Icon}, {@code defaultIcon} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the icon
     * @param defaultIcon
     *                the icon to return if the icon specified by {@code key}
     *                does not exist
     * @return the {@code Icon} object
     * @throws NullPointerException
     *                 if {@code key} or {@code defaultIcon} is {@code null}
     */
    public static Icon getSafeIcon(Object key, Icon defaultIcon) {
        Contract.asNotNull(defaultIcon, "defaultIcon cannot be null");
        
        Icon safeIcon = UIManager.getIcon(key);
        
        if (safeIcon == null) {
            safeIcon = defaultIcon;
        }
        
        if (!(safeIcon instanceof UIResource)) {
            safeIcon = new IconUIResource(safeIcon);
        }
        
        return safeIcon;
    }
    
    /**
     * Returns an insets from the defaults. If the value for {@code key} is not
     * a {@code Insets}, {@code defaultInsets} is returned.
     * 
     * @param key
     *                an {@code Object} specifying the insets
     * @param defaultInsets
     *                the insets to return if the insets specified by
     *                {@code key} does not exist
     * @return the {@code Insets} object
     * @throws NullPointerException
     *                 if {@code key} or {@code defaultInsets} is {@code null}
     */
    public static Insets getSafeInsets(Object key, Insets defaultInsets) {
        Contract.asNotNull(defaultInsets, "defaultInsets cannot be null");
        
        Insets safeInsets = UIManager.getInsets(key);
        
        if (safeInsets == null) {
            safeInsets = defaultInsets;
        }
        
        if (!(safeInsets instanceof UIResource)) {
            safeInsets = new InsetsUIResource(safeInsets.top, safeInsets.left,
                    safeInsets.bottom, safeInsets.right);
        }
        
        return safeInsets;
    }
}
