/*
 * $Id: LookAndFeelAddons.java 4109 2012-01-20 15:23:31Z kschaefe $
 *
 * Copyright 2004 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.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.JComponent;
import javax.swing.LookAndFeel;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.UIResource;

import org.jdesktop.swingx.painter.Painter;

/**
 * Provides additional pluggable UI for new components added by the library. By default, the library
 * uses the pluggable UI returned by {@link #getBestMatchAddonClassName()}.
 * <p>
 * The default addon can be configured using the <code>swing.addon</code> system property as follow:
 * <ul>
 * <li>on the command line, <code>java -Dswing.addon=ADDONCLASSNAME ...</code></li>
 * <li>at runtime and before using the library components
 * <code>System.getProperties().put("swing.addon", ADDONCLASSNAME);</code></li>
 * </ul>
 * <p>
 * The default {@link #getCrossPlatformAddonClassName() cross platform addon} can be configured
 * using the <code>swing.crossplatformlafaddon</code> system property as follow:
 * <ul>
 * <li>on the command line, <code>java -Dswing.crossplatformlafaddon=ADDONCLASSNAME ...</code></li>
 * <li>at runtime and before using the library components
 * <code>System.getProperties().put("swing.crossplatformlafaddon", ADDONCLASSNAME);</code> <br>
 * Note: changing this property after the UI has been initialized may result in unexpected behavior.
 * </li>
 * </ul>
 * <p>
 * The addon can also be installed directly by calling the {@link #setAddon(String)}method. For
 * example, to install the Windows addons, add the following statement
 * <code>LookAndFeelAddons.setAddon("org.jdesktop.swingx.plaf.windows.WindowsLookAndFeelAddons");</code>.
 * 
 * @author <a href="mailto:fred@L2FProd.com">Frederic Lavigne</a>
 * @author Karl Schaefer
 */
@SuppressWarnings("nls")
public abstract class LookAndFeelAddons {

    private static List<ComponentAddon> contributedComponents = new ArrayList<ComponentAddon>();

    /**
     * Key used to ensure the current UIManager has been populated by the LookAndFeelAddons.
     */
    private static final Object APPCONTEXT_INITIALIZED = new Object();

    private static boolean trackingChanges = false;
    private static PropertyChangeListener changeListener;

    static {
        // load the default addon
        String addonClassname = getBestMatchAddonClassName();

        try {
            addonClassname = System.getProperty("swing.addon", addonClassname);
        } catch (SecurityException e) {
            // security exception may arise in Java Web Start
        }

        try {
            setAddon(addonClassname);
        } catch (Exception e) {
            // PENDING(fred) do we want to log an error and continue with a default
            // addon class or do we just fail?
            throw new ExceptionInInitializerError(e);
        }

        setTrackingLookAndFeelChanges(true);
    }

    private static LookAndFeelAddons currentAddon;

    /**
     * Determines if the addon is a match for the {@link UIManager#getLookAndFeel() current Look and
     * Feel}.
     * 
     * @return {@code true} if this addon matches (is compatible); {@code false} otherwise
     */
    protected boolean matches() {
        return false;
    }

    /**
     * Determines if the addon is a match for the system Look and Feel.
     * 
     * @return {@code true} if this addon matches (is compatible with) the system Look and Feel;
     *         {@code false} otherwise
     */
    protected boolean isSystemAddon() {
        return false;
    }

    /**
     * Initializes the look and feel addon. This method is
     * 
     * @see #uninitialize
     * @see UIManager#setLookAndFeel
     */
    public void initialize() {
        for (Iterator<ComponentAddon> iter = contributedComponents.iterator(); iter.hasNext();) {
            ComponentAddon addon = iter.next();
            addon.initialize(this);
        }
    }

    public void uninitialize() {
        for (Iterator<ComponentAddon> iter = contributedComponents.iterator(); iter.hasNext();) {
            ComponentAddon addon = iter.next();
            addon.uninitialize(this);
        }
    }

    /**
     * Adds the given defaults in UIManager.
     * 
     * Note: the values are added only if they do not exist in the existing look and feel defaults.
     * This makes it possible for look and feel implementors to override SwingX defaults.
     * 
     * Note: the array is traversed in reverse order. If a key is found twice in the array, the
     * key/value with the highest position in the array gets precedence over the other key in the
     * array
     * 
     * @param keysAndValues
     */
    public void loadDefaults(Object[] keysAndValues) {
        // Go in reverse order so the most recent keys get added first...
        for (int i = keysAndValues.length - 2; i >= 0; i = i - 2) {
            if (UIManager.getLookAndFeelDefaults().get(keysAndValues[i]) == null) {
                UIManager.getLookAndFeelDefaults().put(keysAndValues[i], keysAndValues[i + 1]);
            }
        }
    }

    public void unloadDefaults(@SuppressWarnings("unused") Object[] keysAndValues) {
        // commented after Issue 446.
        /*
         * for (int i = 0, c = keysAndValues.length; i < c; i = i + 2) {
         * UIManager.getLookAndFeelDefaults().put(keysAndValues[i], null); }
         */
    }

    public static void setAddon(String addonClassName) throws InstantiationException,
            IllegalAccessException, ClassNotFoundException {
        setAddon(Class.forName(addonClassName));
    }

    public static void setAddon(Class<?> addonClass) throws InstantiationException,
            IllegalAccessException {
        LookAndFeelAddons addon = (LookAndFeelAddons) addonClass.newInstance();
        setAddon(addon);
    }

    public static void setAddon(LookAndFeelAddons addon) {
        if (currentAddon != null) {
            currentAddon.uninitialize();
        }

        addon.initialize();
        currentAddon = addon;
        // JW: we want a marker to discover if the LookAndFeelDefaults have been
        // swept from under our feet. The following line looks suspicious,
        // as it is setting a user default instead of a LF default. User defaults
        // are not touched when resetting a LF
        UIManager.put(APPCONTEXT_INITIALIZED, Boolean.TRUE);
        // trying to fix #784-swingx: frequent NPE on getUI
        // JW: we want a marker to discover if the LookAndFeelDefaults have been
        // swept from under our feet.
        UIManager.getLookAndFeelDefaults().put(APPCONTEXT_INITIALIZED, Boolean.TRUE);
    }

    public static LookAndFeelAddons getAddon() {
        return currentAddon;
    }

    private static ClassLoader getClassLoader() {
        ClassLoader cl = null;
        
        try {
            cl = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
                @Override
                public ClassLoader run() {
                    return LookAndFeelAddons.class.getClassLoader();
                }
            });
        } catch (SecurityException ignore) { }
        
        if (cl == null) {
            final Thread t = Thread.currentThread();
            
            try {
                cl = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
                    @Override
                    public ClassLoader run() {
                        return t.getContextClassLoader();
                    }
                });
            } catch (SecurityException ignore) { }
        }
        
        if (cl == null) {
            try {
                cl = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
                    @Override
                    public ClassLoader run() {
                        return ClassLoader.getSystemClassLoader();
                    }
                });
            } catch (SecurityException ignore) { }
        }
        
        return cl;
    }
    
    /**
     * Based on the current look and feel (as returned by <code>UIManager.getLookAndFeel()</code>),
     * this method returns the name of the closest <code>LookAndFeelAddons</code> to use.
     * 
     * @return the addon matching the currently installed look and feel
     */
    public static String getBestMatchAddonClassName() {
        LookAndFeel laf = UIManager.getLookAndFeel();
        String className = null;

        if (UIManager.getCrossPlatformLookAndFeelClassName().equals(laf.getClass().getName())) {
            className = getCrossPlatformAddonClassName();
        } else if (UIManager.getSystemLookAndFeelClassName().equals(laf.getClass().getName())) {
            className = getSystemAddonClassName();
        } else {
            ServiceLoader<LookAndFeelAddons> addonLoader = ServiceLoader.load(LookAndFeelAddons.class,
                    getClassLoader());

            for (LookAndFeelAddons addon : addonLoader) {
                if (addon.matches()) {
                    className = addon.getClass().getName();
                    break;
                }
            }
        }

        if (className == null) {
            className = getSystemAddonClassName();
        }

        return className;
    }

    public static String getCrossPlatformAddonClassName() {
        try {
            return AccessController.doPrivileged(new PrivilegedAction<String>() {
                @Override
                public String run() {
                    return System.getProperty("swing.crossplatformlafaddon",
                            "org.jdesktop.swingx.plaf.metal.MetalLookAndFeelAddons");
                }
            });
        } catch (SecurityException ignore) {
        }

        return "org.jdesktop.swing.plaf.metal.MetalLookAndFeelAddons";
    }

    /**
     * Gets the addon best suited for the operating system where the virtual machine is running.
     * 
     * @return the addon matching the native operating system platform.
     */
    public static String getSystemAddonClassName() {
        ServiceLoader<LookAndFeelAddons> addonLoader = ServiceLoader.load(LookAndFeelAddons.class,
                getClassLoader());
        String className = null;

        for (LookAndFeelAddons addon : addonLoader) {
            if (addon.isSystemAddon()) {
                className = addon.getClass().getName();
                break;
            }
        }

        if (className == null) {
            className = getCrossPlatformAddonClassName();
        }

        return className;
    }

    /**
     * Each new component added by the library will contribute its default UI classes, colors and
     * fonts to the LookAndFeelAddons. See {@link ComponentAddon}.
     * 
     * @param component
     */
    public static void contribute(ComponentAddon component) {
        contributedComponents.add(component);

        if (currentAddon != null) {
            // make sure to initialize any addons added after the
            // LookAndFeelAddons has been installed
            component.initialize(currentAddon);
        }
    }

    /**
     * Removes the contribution of the given addon
     * 
     * @param component
     */
    public static void uncontribute(ComponentAddon component) {
        contributedComponents.remove(component);

        if (currentAddon != null) {
            component.uninitialize(currentAddon);
        }
    }

    /**
     * Workaround for IDE mixing up with classloaders and Applets environments. Consider this method
     * as API private. It must not be called directly.
     * 
     * @param component
     * @param expectedUIClass
     * @return an instance of expectedUIClass
     */
    public static ComponentUI getUI(JComponent component, Class<?> expectedUIClass) {
        maybeInitialize();

        // solve issue with ClassLoader not able to find classes
        String uiClassname = (String) UIManager.get(component.getUIClassID());
        // possible workaround and more debug info on #784
        if (uiClassname == null) {
            Logger logger = Logger.getLogger("LookAndFeelAddons");
            logger.warning("Failed to retrieve UI for " + component.getClass().getName()
                    + " with UIClassID " + component.getUIClassID());
            if (logger.isLoggable(Level.FINE)) {
                logger.fine("Existing UI defaults keys: "
                        + new ArrayList<Object>(UIManager.getDefaults().keySet()));
            }
            // really ugly hack. Should be removed as soon as we figure out what is causing the
            // issue
            uiClassname = "org.jdesktop.swingx.plaf.basic.Basic" + expectedUIClass.getSimpleName();
        }
        try {
            Class<?> uiClass = Class.forName(uiClassname);
            UIManager.put(uiClassname, uiClass);
        } catch (ClassNotFoundException e) {
            // we ignore the ClassNotFoundException
        }

        ComponentUI ui = UIManager.getUI(component);

        if (expectedUIClass.isInstance(ui)) {
            return ui;
        } else if (ui == null) {
            barkOnUIError("no ComponentUI class for: " + component);
        } else {
            String realUI = ui.getClass().getName();
            Class<?> realUIClass = null;

            try {
                realUIClass = expectedUIClass.getClassLoader().loadClass(realUI);
            } catch (ClassNotFoundException e) {
                barkOnUIError("failed to load class " + realUI);
            }

            if (realUIClass != null) {
                try {
                    Method createUIMethod = realUIClass.getMethod("createUI",
                            new Class[] { JComponent.class });

                    return (ComponentUI) createUIMethod.invoke(null, new Object[] { component });
                } catch (NoSuchMethodException e) {
                    barkOnUIError("static createUI() method not found in " + realUIClass);
                } catch (Exception e) {
                    barkOnUIError("createUI() failed for " + component + " " + e);
                }
            }
        }

        return null;
    }

    // this is how core UIDefaults yells about bad components; we do the same
    private static void barkOnUIError(String message) {
        System.err.println(message);
        new Error().printStackTrace();
    }

    /**
     * With applets, if you reload the current applet, the UIManager will be reinitialized (entries
     * previously added by LookAndFeelAddons will be removed) but the addon will not reinitialize
     * because addon initialize itself through the static block in components and the classes do not
     * get reloaded. This means component.updateUI will fail because it will not find its UI.
     * 
     * This method ensures LookAndFeelAddons get re-initialized if needed. It must be called in
     * every component updateUI methods.
     */
    private static synchronized void maybeInitialize() {
        if (currentAddon != null) {
            // this is to ensure "UIManager#maybeInitialize" gets called and the
            // LAFState initialized
            UIDefaults defaults = UIManager.getLookAndFeelDefaults();
            // if (!UIManager.getBoolean(APPCONTEXT_INITIALIZED)) {
            // JW: trying to fix #784-swingx: frequent NPE in getUI
            // moved the "marker" property into the LookAndFeelDefaults
            if (!defaults.getBoolean(APPCONTEXT_INITIALIZED)) {
                setAddon(currentAddon);
            }
        }
    }

    //
    // TRACKING OF THE CURRENT LOOK AND FEEL
    //
    private static class UpdateAddon implements PropertyChangeListener {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            try {
                setAddon(getBestMatchAddonClassName());
            } catch (Exception e) {
                // should not happen
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * If true, everytime the Swing look and feel is changed, the addon which best matches the
     * current look and feel will be automatically selected.
     * 
     * @param tracking
     *            true to automatically update the addon, false to not automatically track the
     *            addon. Defaults to false.
     * @see #getBestMatchAddonClassName()
     */
    public static synchronized void setTrackingLookAndFeelChanges(boolean tracking) {
        if (trackingChanges != tracking) {
            if (tracking) {
                if (changeListener == null) {
                    changeListener = new UpdateAddon();
                }
                UIManager.addPropertyChangeListener(changeListener);
            } else {
                if (changeListener != null) {
                    UIManager.removePropertyChangeListener(changeListener);
                }
                changeListener = null;
            }
            trackingChanges = tracking;
        }
    }

    /**
     * @return true if the addon will be automatically change to match the current look and feel
     * @see #setTrackingLookAndFeelChanges(boolean)
     */
    public static synchronized boolean isTrackingLookAndFeelChanges() {
        return trackingChanges;
    }

    /**
     * Convenience method for setting a component's background painter property with a value from
     * the defaults. The painter is only set if the painter is {@code null} or an instance of
     * {@code UIResource}.
     * 
     * @param c
     *            component to set the painter on
     * @param painter
     *            key specifying the painter
     * @throws NullPointerException
     *             if the component or painter is {@code null}
     * @throws IllegalArgumentException
     *             if the component does not contain the "backgroundPainter" property or the
     *             property cannot be set
     */
    public static void installBackgroundPainter(JComponent c, String painter) {
        Class<?> clazz = c.getClass();

        try {
            Method getter = clazz.getMethod("getBackgroundPainter");
            Method setter = clazz.getMethod("setBackgroundPainter", Painter.class);

            Painter<?> p = (Painter<?>) getter.invoke(c);

            if (p == null || p instanceof UIResource) {
                setter.invoke(c, UIManagerExt.getPainter(painter));
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new IllegalArgumentException("cannot set painter on " + c.getClass());
        }
    }

    /**
     * Convenience method for uninstalling a background painter. If the painter of the component is
     * a {@code UIResource}, it is set to {@code null}.
     * 
     * @param c
     *            component to uninstall the painter on
     * @throws NullPointerException
     *             if {@code c} is {@code null}
     * @throws IllegalArgumentException
     *             if the component does not contain the "backgroundPainter" property or the
     *             property cannot be set
     */
    public static void uninstallBackgroundPainter(JComponent c) {
        Class<?> clazz = c.getClass();

        try {
            Method getter = clazz.getMethod("getBackgroundPainter");
            Method setter = clazz.getMethod("setBackgroundPainter", Painter.class);

            Painter<?> p = (Painter<?>) getter.invoke(c);

            if (p == null || p instanceof UIResource) {
                setter.invoke(c, (Painter<?>) null);
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new IllegalArgumentException("cannot set painter on " + c.getClass());
        }
    }
}
