/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.openide.util;

import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.HeadlessException;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.Transparency;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.ImageObserver;
import java.awt.image.RGBImageFilter;
import java.awt.image.WritableRaster;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;

/** 
 * Useful static methods for manipulation with images/icons, results are cached.
 *
 * <p>Images can be represented as instances of either {@link Image} or {@link Icon}. For best
 * results on HiDPI displays, clients should use the {@link #image2Icon(Image)} method provided by
 * this class when converting an {@code Image} to an {@code Icon}, rather than constructing
 * {@link ImageIcon} instances themselves. When doing manual painting, clients should use
 * {@link Icon#paintIcon(Component, Graphics, int, int)} rather than
 * {@link Graphics#drawImage(Image, int, int, ImageObserver)}.
 * 
 * @author Jaroslav Tulach, Tomas Holy
 * @since 7.15
 */
public final class ImageUtilities {

    private static final Logger LOGGER = Logger.getLogger(ImageUtilities.class.getName());

    /** separator for individual parts of tool tip text */
    static final String TOOLTIP_SEPAR = "<br>"; // NOI18N
    /** a value that indicates that the icon does not exists */
    private static final ActiveRef<String> NO_ICON = new ActiveRef<String>(null, null, null);

    private static final Map<String,ActiveRef<String>> cache = new HashMap<String,ActiveRef<String>>(128);
    private static final Map<String,ActiveRef<String>> localizedCache = new HashMap<String,ActiveRef<String>>(128);
    private static final Map<CompositeImageKey,ActiveRef<CompositeImageKey>> compositeCache = new HashMap<CompositeImageKey,ActiveRef<CompositeImageKey>>(128);
    private static final Map<ToolTipImageKey, ActiveRef<ToolTipImageKey>> imageToolTipCache = new HashMap<ToolTipImageKey, ActiveRef<ToolTipImageKey>>(128);

    private static RGBImageFilter imageIconFilter = null;

    /** Resource paths for which we have had to strip initial slash.
     * @see "#20072"
     */
    private static final Set<String> extraInitialSlashes = new HashSet<String>();
    private static volatile Object currentLoader;
    private static Lookup.Result<ClassLoader> loaderQuery = null;
    private static boolean noLoaderWarned = false;
    private static final Component component = new Component() {
    };

    private static final MediaTracker tracker = new MediaTracker(component);
    private static int mediaTrackerID;
    
    private static ImageReader PNG_READER;
//    private static ImageReader GIF_READER;
    
    private static final Logger ERR = Logger.getLogger(ImageUtilities.class.getName());
    
    private static final String DARK_LAF_SUFFIX = "_dark"; //NOI18N

    /**
     * Dummy component to be passed to the first parameter  of
     * {@link Icon#paintIcon(Component, Graphics, int, int)} when converting an {@code Icon} to an
     * {@code Image}. See comment in {@link #icon2ToolTipImage(Icon)}.
     */
    private static volatile Component dummyIconComponent;

    static {
        /* Could have used Mutex.EVENT.writeAccess here, but it doesn't seem to be available during
        testing. */
        if (EventQueue.isDispatchThread()) {
            dummyIconComponent = new JLabel();
        } else {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    dummyIconComponent = new JLabel();
                }
            });
        }
    }
    
    private ImageUtilities() {
    }

    static {
        ImageIO.setUseCache(false);
        PNG_READER = ImageIO.getImageReadersByMIMEType("image/png").next();
//        GIF_READER = ImageIO.getImageReadersByMIMEType("image/gif").next();
    }

    /**
     * Loads an image from the specified resource ID. The image is loaded using the "system" classloader registered in
     * Lookup.
     * @param resourceID resource path of the icon (no initial slash)
     * @return icon's Image, or null, if the icon cannot be loaded.     
     */
    public static final Image loadImage(String resourceID) {
        return loadImage(resourceID, false);
    }
    
    /**
     * Loads an image based on resource path.
     * Exactly like {@link #loadImage(String)} but may do a localized search.
     * For example, requesting <samp>org/netbeans/modules/foo/resources/foo.gif</samp>
     * might actually find <samp>org/netbeans/modules/foo/resources/foo_ja.gif</samp>
     * or <samp>org/netbeans/modules/foo/resources/foo_mybranding.gif</samp>.
     * 
     * <p>Caching of loaded images can be used internally to improve performance.
     * <p> Since version 8.12 the returned image object responds to call
     * <code>image.getProperty("url", null)</code> by returning the internal
     * {@link URL} of the found and loaded <code>resource</code>.
     * 
     * <p>If the current look and feel is 'dark' (<code>UIManager.getBoolean("nb.dark.theme")</code>)
     * then the method first attempts to load image <i>&lt;original file name&gt;<b>_dark</b>.&lt;original extension&gt;</i>.
     * If such file doesn't exist the default one is loaded instead.
     * </p>
     * 
     * @param resource resource path of the image (no initial slash)
     * @param localized true for localized search
     * @return icon's Image or null if the icon cannot be loaded
     */
    public static final Image loadImage(String resource, boolean localized) {
        return loadImageInternal(resource, localized);
    }

    /* Private version of the method showing the more specific return type. We always return a
    ToolTipImage, to take advantage of its rendering tweaks for HiDPI screens. */
    private static ToolTipImage loadImageInternal(String resource, boolean localized) {
        // Avoid a NPE that could previously occur in the isDarkLaF case only. See NETBEANS-2401.
        if (resource == null) {
            return null;
        }
        ToolTipImage image = null;
        if( isDarkLaF() ) {
            image = getIcon(addDarkSuffix(resource), localized);
            // found an image with _dark-suffix, so there no need to apply an
            // image filter to make it look nice using dark themes
        }
        if (null == image) {
            image = getIcon(resource, localized);
            // only non _dark images need filtering
            RGBImageFilter imageFilter = getImageIconFilter();
            if (null != image && null != imageFilter) {
                image = icon2ToolTipImage(FilteredIcon.create(imageFilter, image));
            }
        }
        return image;
    }

    /**
     * Loads an icon based on resource path.
     * Similar to {@link #loadImage(String, boolean)}, returns ImageIcon instead of Image.
     * 
     * <p>If the current look and feel is 'dark' (<code>UIManager.getBoolean("nb.dark.theme")</code>)
     * then the method first attempts to load image <i>&lt;original file name&gt;<b>_dark</b>.&lt;original extension&gt;</i>.
     * If such file doesn't exist the default one is loaded instead.
     * </p>
     * 
     * @param resource resource path of the icon (no initial slash)
     * @param localized localized resource should be used
     * @return ImageIcon or null, if the icon cannot be loaded.
     * @since 7.22
     */
    public static final ImageIcon loadImageIcon( String resource, boolean localized ) {
        ToolTipImage image = loadImageInternal(resource, localized);
        if( image == null ) {
            return null;
        }
        return image.asImageIcon();
    }
    
    private static boolean isDarkLaF() {
        return UIManager.getBoolean("nb.dark.theme"); //NOI18N 
    }
    
    /**
     * 
     * @param resourceName
     * @return 
     * @since 8.35
     */
    private static String addDarkSuffix( String resourceName ) {
        int dotIndex = resourceName.lastIndexOf('.');
        if( dotIndex > 0 ) {
            return resourceName.substring(0, dotIndex) + DARK_LAF_SUFFIX + resourceName.substring(dotIndex);
        }
        return resourceName + DARK_LAF_SUFFIX;
    }

    private static RGBImageFilter getImageIconFilter() {
        if( null == imageIconFilter ) {
            Object obj = UIManager.get( "nb.imageicon.filter"); //NOI18N
            if( obj instanceof RGBImageFilter ) {
                imageIconFilter = ( RGBImageFilter ) obj;
            }
        }
        return imageIconFilter;
    }

    /** This method merges two images into the new one. The second image is drawn
     * over the first one with its top-left corner at x, y. Images need not be of the same size.
     * New image will have a size of max(second image size + top-left corner, first image size).
     * Method is used mostly when second image contains transparent pixels (e.g. for badging).
     * Method that attempts to find the merged image in the cache first, then
     * creates the image if it was not found.
     * @param image1 underlying image
     * @param image2 second image
     * @param x x position of top-left corner
     * @param y y position of top-left corner
     * @return new merged image
     */    
    public static final Image mergeImages(Image image1, Image image2, int x, int y) {
        if (image1 == null || image2 == null) {
            throw new NullPointerException();
        }
        
        CompositeImageKey k = new CompositeImageKey(image1, image2, x, y);
        ToolTipImage cached;

        synchronized (compositeCache) {
            ActiveRef<CompositeImageKey> r = compositeCache.get(k);
            if (r != null) {
                cached = r.get();
                if (cached != null) {
                    return cached;
                }
            }
            cached = doMergeImages(image1, image2, x, y);
            compositeCache.put(k, new ActiveRef<CompositeImageKey>(cached, compositeCache, k));
            return cached;
        }
    }    
    
    /**
     * Converts given image to an icon.
     * @param image to be converted
     * @return icon corresponding icon
     */    
    public static final Icon image2Icon(Image image) {
        /* Make sure to always return a ToolTipImage, to take advantage of its rendering tweaks for
        HiDPI screens. */
        return (image instanceof ToolTipImage)
                ? (ToolTipImage) image : assignToolTipToImageInternal(image, "");
    }
    
    /**
     * Converts given icon to a {@link java.awt.Image}.
     *
     * <p>A scalable {@link Icon} instance can always be recovered by passing the returned
     * {@code Image} to {@link #image2Icon(Image)} again, i.e. for painting on HiDPI screens.
     *
     * @param icon {@link javax.swing.Icon} to be converted.
     */
    public static final Image icon2Image(Icon icon) {
        if (icon == null) {
            LOGGER.log(Level.WARNING, null, new NullPointerException());
            return loadImage("org/openide/nodes/defaultNode.png", true);
        }
        if (icon instanceof ToolTipImage) {
            return (ToolTipImage) icon;
        } else if (icon instanceof IconImageIcon) {
            return icon2Image(((IconImageIcon) icon).getDelegateIcon());
        } else if (icon instanceof ImageIcon) {
            Image ret = ((ImageIcon) icon).getImage();
            if (ret != null)
                return ret;
        }
        return icon2ToolTipImage(icon);
    }

    private static ToolTipImage icon2ToolTipImage(Icon icon) {
        Parameters.notNull("icon", icon);
        if (icon instanceof ToolTipImage) {
            return (ToolTipImage) icon;
        }
        ToolTipImage image = new ToolTipImage(icon, "", BufferedImage.TYPE_INT_ARGB);
        Graphics g = image.getGraphics();
        /* Previously, we'd create a new JLabel here every time; this once led to a deadlock on
        startup when the nb.imageicon.filter setting was enabled. The underlying problem is that
        methods in this class may be called from any thread, while JLabel's methods and constructors
        should really only be called on the Event Dispatch Thread. Constructing the component once
        on the EDT fixed the problem. Read-only operations from non-EDT threads shouldn't really be
        a problem; most Icon implementations won't ever access the component parameter anyway. */
        icon.paintIcon(dummyIconComponent, g, 0, 0);
        g.dispose();
        return image;
    }
    
    /**
     * Assign tool tip text to given image (creates new or returns cached, original remains unmodified)
     * Text can contain HTML tags e.g. "&#60;b&#62;my&#60;/b&#62; text"
     * @param image image to which tool tip should be set
     * @param text tool tip text
     * @return Image with attached tool tip 
     */    
    public static final Image assignToolTipToImage(Image image, String text) {
        return assignToolTipToImageInternal(image, text);
    }

    // Private version with more specific return type.
    private static ToolTipImage assignToolTipToImageInternal(Image image, String text) {
        Parameters.notNull("image", image);
        Parameters.notNull("text", text);
        ToolTipImageKey key = new ToolTipImageKey(image, text);
        ToolTipImage cached;
        synchronized (imageToolTipCache) {
            ActiveRef<ToolTipImageKey> r = imageToolTipCache.get(key);
            if (r != null) {
                cached = r.get();
                if (cached != null) {
                    return cached;
                }
            }
            cached = ToolTipImage.createNew(text, image, null);
            imageToolTipCache.put(key, new ActiveRef<ToolTipImageKey>(cached, imageToolTipCache, key));
            return cached;
        }
    }

    /**
     * Get tool tip text for given image
     * @param image image which is asked for tool tip text
     * @return String containing attached tool tip text
     */
    public static final String getImageToolTip(Image image) {
        if (image instanceof ToolTipImage) {
            return ((ToolTipImage) image).toolTipText;
        } else {
            return "";
        }
    }

    /**
     * Add text to tool tip for given image (creates new or returns cached, original remains unmodified)
     * Text can contain HTML tags e.g. "&#60;b&#62;my&#60;/b&#62; text"
     * @param text text to add to tool tip
     * @return Image with attached tool tip
     */
    public static final Image addToolTipToImage(Image image, String text) {
        if (image instanceof ToolTipImage) {
            ToolTipImage tti = (ToolTipImage) image;
            StringBuilder str = new StringBuilder(tti.toolTipText);
            if (str.length() > 0 && text.length() > 0) {
                str.append(TOOLTIP_SEPAR);
            }
            str.append(text);
            return assignToolTipToImage(image, str.toString());
        } else {
            return assignToolTipToImage(image, text);
        }
    }

    /**
     * Creates disabled (color saturation lowered) icon.
     * Icon image conversion is performed lazily.
     * @param icon original icon used for conversion
     * @return less saturated Icon
     * @since 7.28
     */
    public static Icon createDisabledIcon(Icon icon)  {
        Parameters.notNull("icon", icon);
        /* FilteredIcon's Javadoc mentions a caveat about the Component parameter that is passed to
        Icon.paintIcon. It's not really a problem; previous implementations had the same
        behavior. */
        return FilteredIcon.create(DisabledButtonFilter.INSTANCE, icon);
    }

    /**
     * Creates disabled (color saturation lowered) image.
     * @param image original image used for conversion
     * @return less saturated Image
     * @since 7.28
     */
    public static Image createDisabledImage(Image image)  {
        Parameters.notNull("image", image);
        // Go through FilteredIcon to preserve scalable icons.
        return icon2Image(createDisabledIcon(image2Icon(image)));
    }

    /**
     * Get the class loader from lookup.
     * Since this is done very frequently, it is wasteful to query lookup each time.
     * Instead, remember the last result and just listen for changes.
     */
    static ClassLoader getLoader() {
        Object is = currentLoader;
        if (is instanceof ClassLoader) {
            return (ClassLoader)is;
        }
            
        currentLoader = Thread.currentThread();
            
        if (loaderQuery == null) {
            loaderQuery = Lookup.getDefault().lookup(new Lookup.Template<ClassLoader>(ClassLoader.class));
            loaderQuery.addLookupListener(
                new LookupListener() {
                    public void resultChanged(LookupEvent ev) {
                        ERR.fine("Loader cleared"); // NOI18N
                        currentLoader = null;
                    }
                }
            );
        }

        Iterator it = loaderQuery.allInstances().iterator();
        if (it.hasNext()) {
            ClassLoader toReturn = (ClassLoader) it.next();
            if (currentLoader == Thread.currentThread()) {
                currentLoader = toReturn;
            }
            if (ERR.isLoggable(Level.FINE)) {
                ERR.fine("Loader computed: " + currentLoader); // NOI18N
            }
            return toReturn;
        } else { if (!noLoaderWarned) {
                noLoaderWarned = true;
                ERR.warning(
                    "No ClassLoader instance found in " + Lookup.getDefault() // NOI18N
                );
            }
            return null;
        }
    }

    static ToolTipImage getIcon(String resource, boolean localized) {
        if (localized) {
            if (resource == null) {
                return null;
            }
            synchronized (localizedCache) {
                ActiveRef<String> ref = localizedCache.get(resource);
                ToolTipImage img = null;

                // no icon for this name (already tested)
                if (ref == NO_ICON) {
                    return null;
                }

                if (ref != null) {
                    // then it is SoftRefrence
                    img = ref.get();
                }

                // icon found
                if (img != null) {
                    return img;
                }

                // find localized or base image
                ClassLoader loader = getLoader();

                // we'll keep the String probably for long time, optimize it
                resource = new String(resource).intern(); // NOPMD

                String base;
                String ext;
                int idx = resource.lastIndexOf('.');

                if ((idx != -1) && (idx > resource.lastIndexOf('/'))) {
                    base = resource.substring(0, idx);
                    ext = resource.substring(idx);
                } else {
                    base = resource;
                    ext = ""; // NOI18N
                }

                // #31008. [PENDING] remove in case package cache is precomputed
                java.net.URL baseurl = (loader != null) ? loader.getResource(resource) // NOPMD
                        : ImageUtilities.class.getClassLoader().getResource(resource);
                Iterator<String> it = NbBundle.getLocalizingSuffixes();
                
                while (it.hasNext()) {
                    String suffix = it.next();
                    ToolTipImage i;

                    if (suffix.length() == 0) {
                        i = getIcon(resource, loader, false);
                    } else {
                        i = getIcon(base + suffix + ext, loader, true);
                    }

                    if (i != null) {
                        localizedCache.put(resource, new ActiveRef<String>(i, localizedCache, resource));
                        return i;
                    }
                }
                localizedCache.put(resource, NO_ICON);
                return null;
            }
        } else {
            return getIcon(resource, getLoader(), false);
        }
    }

    /** Finds image for given resource.
    * @param name name of the resource
    * @param loader classloader to use for locating it, or null to use classpath
    * @param localizedQuery whether the name contains some localization suffix
    *  and is not optimized/interned
    */
    private static ToolTipImage getIcon(String name, ClassLoader loader, boolean localizedQuery) {
        if (name == null) {
            return null;
        }
        ActiveRef<String> ref = cache.get(name);
        ToolTipImage img = null;

        // no icon for this name (already tested)
        if (ref == NO_ICON) {
            return null;
        }

        if (ref != null) {
            img = ref.get();
        }

        // icon found
        if (img != null) {
            return img;
        }

        synchronized (cache) {
            // again under the lock
            ref = cache.get(name);

            // no icon for this name (already tested)
            if (ref == NO_ICON) {
                return null;
            }

            if (ref != null) {
                // then it is SoftRefrence
                img = ref.get();
            }

            if (img != null) {
                // cannot be NO_ICON, since it never disappears from the map.
                return img;
            }

            // path for bug in classloader
            String n;
            boolean warn;

            if (name.startsWith("/")) { // NOI18N
                warn = true;
                n = name.substring(1);
            } else {
                warn = false;
                n = name;
            }

            // we have to load it
            java.net.URL url = (loader != null) ? loader.getResource(n)
                                                : ImageUtilities.class.getClassLoader().getResource(n);

//            img = (url == null) ? null : Toolkit.getDefaultToolkit().createImage(url);
            Image result = null;
            try {
                if (url != null) {
                    if (name.endsWith(".png")) {
                        ImageInputStream stream = ImageIO.createImageInputStream(url.openStream());
                        ImageReadParam param = PNG_READER.getDefaultReadParam();
                        try {
                            PNG_READER.setInput(stream, true, true);
                            result = PNG_READER.read(0, param);
                        }
                        catch (IOException ioe1) {
                            ERR.log(Level.INFO, "Image "+name+" is not PNG", ioe1);
                        }
                        stream.close();
                    } 
                    /*
                    else if (name.endsWith(".gif")) {
                        ImageInputStream stream = ImageIO.createImageInputStream(url.openStream());
                        ImageReadParam param = GIF_READER.getDefaultReadParam();
                        try {
                            GIF_READER.setInput(stream, true, true);
                            result = GIF_READER.read(0, param);
                        }
                        catch (IOException ioe1) {
                            ERR.log(Level.INFO, "Image "+name+" is not GIF", ioe1);
                        }
                        stream.close();
                    }
                     */

                    if (result == null) {
                        result = ImageIO.read(url);
                    }
                }
            } catch (IOException ioe) {
                ERR.log(Level.WARNING, "Cannot load " + name + " image", ioe);
            }

            if (result != null) {
                if (warn && extraInitialSlashes.add(name)) {
                    ERR.warning(
                        "Initial slashes in Utilities.loadImage deprecated (cf. #20072): " +
                        name
                    ); // NOI18N
                }

//                Image img2 = toBufferedImage(result);

                if (ERR.isLoggable(Level.FINE)) {
                    ERR.log(Level.FINE, "loading icon {0} = {1}", new Object[] {n, result});
                }
                name = new String(name).intern(); // NOPMD
                ToolTipImage toolTipImage = ToolTipImage.createNew("", result, url);
                cache.put(name, new ActiveRef<String>(toolTipImage, cache, name));
                return toolTipImage;
            } else { // no icon found
                if (!localizedQuery) {
                    cache.put(name, NO_ICON);
                }
                return null;
            }
        }
    }

    /** The method creates a BufferedImage which represents the same Image as the
     * parameter but consumes less memory.
     */
    static final Image toBufferedImage(Image img) {
        // load the image
        new javax.swing.ImageIcon(img, "");

        if (img.getHeight(null)*img.getWidth(null) > 24*24) {
            return img;
        }
        java.awt.image.BufferedImage rep = createBufferedImage(img.getWidth(null), img.getHeight(null));
        java.awt.Graphics g = rep.createGraphics();
        g.drawImage(img, 0, 0, null);
        g.dispose();
        img.flush();

        return rep;
    }

    private static void ensureLoaded(Image image) {
        if (
            (Toolkit.getDefaultToolkit().checkImage(image, -1, -1, null) &
                (ImageObserver.ALLBITS | ImageObserver.FRAMEBITS)) != 0
        ) {
            return;
        }

        synchronized (tracker) {
            int id = ++mediaTrackerID;
            tracker.addImage(image, id);

            try {
                tracker.waitForID(id, 0);
            } catch (InterruptedException e) {
                System.out.println("INTERRUPTED while loading Image");
            }

            // #262804 assertation disabled because of error, when using ImageFilter
            // assert (tracker.statusID(id, false) == MediaTracker.COMPLETE) : "Image loaded";
            tracker.removeImage(image, id);
        }
    }
    
    private static final ToolTipImage doMergeImages(Image image1, Image image2, int x, int y) {
        ensureLoaded(image1);
        ensureLoaded(image2);

        int w = Math.max(image1.getWidth(null), x + image2.getWidth(null));
        int h = Math.max(image1.getHeight(null), y + image2.getHeight(null));
        boolean bitmask = (image1 instanceof Transparency) && ((Transparency)image1).getTransparency() != Transparency.TRANSLUCENT
                && (image2 instanceof Transparency) && ((Transparency)image2).getTransparency() != Transparency.TRANSLUCENT;

        StringBuilder str = new StringBuilder(image1 instanceof ToolTipImage ? ((ToolTipImage)image1).toolTipText : "");
        if (image2 instanceof ToolTipImage) {
            String toolTip = ((ToolTipImage)image2).toolTipText;
            if (str.length() > 0 && toolTip.length() > 0) {
                str.append(TOOLTIP_SEPAR);
            }
            str.append(toolTip);
        }
        Object firstUrl = image1.getProperty("url", null);
        
        ColorModel model = colorModel(bitmask? Transparency.BITMASK: Transparency.TRANSLUCENT);
        // Provide a delegate Icon for scalable rendering.
        Icon delegateIcon = new MergedIcon(image2Icon(image1), image2Icon(image2), x, y);
        ToolTipImage buffImage = new ToolTipImage(str.toString(), delegateIcon,
                model, model.createCompatibleWritableRaster(w, h), model.isAlphaPremultiplied(), null, firstUrl instanceof URL ? (URL)firstUrl : null
            );

        // Also provide an Image-based rendering for backwards-compatibility.
        java.awt.Graphics g = buffImage.createGraphics();
        g.drawImage(image1, 0, 0, null);
        g.drawImage(image2, x, y, null);
        g.dispose();

        return buffImage;
    }

    /**
     * Alternative image merging implementation using the {@link Icon} API. This preserves
     * scalability of the delegate {@code Icon}s on HiDPI displays.
     */
    private static final class MergedIcon extends CachedHiDPIIcon {
        private final Icon icon1;
        private final Icon icon2;
        private final int x, y;

        public MergedIcon(Icon icon1, Icon icon2, int x, int y) {
            super(Math.max(icon1.getIconWidth(), x + icon2.getIconWidth()),
                  Math.max(icon1.getIconHeight(), y + icon2.getIconHeight()));
            this.icon1 = icon1;
            this.icon2 = icon2;
            this.x = x;
            this.y = y;
        }

        @Override
        protected Image createImage(Component c, GraphicsConfiguration graphicsConfiguration,
                int deviceWidth, int deviceHeight, double scale)
        {
            BufferedImage ret = graphicsConfiguration.createCompatibleImage(
                    deviceWidth, deviceHeight, Transparency.TRANSLUCENT);
            Graphics2D g = ret.createGraphics();
            try {
                g.clip(new Rectangle(0, 0, deviceWidth, deviceHeight));
                g.scale(scale, scale);
                icon1.paintIcon(c, g, 0, 0);
                icon2.paintIcon(c, g, x, y);
            } finally {
                g.dispose();
            }
            return ret;
        }
    }

    /** Creates BufferedImage with Transparency.TRANSLUCENT */
    static final java.awt.image.BufferedImage createBufferedImage(int width, int height) {
        if (Utilities.isMac()) {
            return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE);
        }

        ColorModel model = colorModel(java.awt.Transparency.TRANSLUCENT);
        java.awt.image.BufferedImage buffImage = new java.awt.image.BufferedImage(
                model, model.createCompatibleWritableRaster(width, height), model.isAlphaPremultiplied(), null
            );

        return buffImage;
    }
    
    static private ColorModel colorModel(int transparency) {
        ColorModel model;
        try {
            model = java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment()
                .getDefaultScreenDevice().getDefaultConfiguration()
                .getColorModel(transparency);
        }
        catch(ArrayIndexOutOfBoundsException aioobE) {
            //#226279
            model = ColorModel.getRGBdefault();
        } catch(HeadlessException he) {
            model = ColorModel.getRGBdefault();
        }
        return model;
    }

    /**
     * Key used for composite images -- it holds image identities
     */
    private static class CompositeImageKey {
        Image baseImage;
        Image overlayImage;
        int x;
        int y;

        CompositeImageKey(Image base, Image overlay, int x, int y) {
            this.x = x;
            this.y = y;
            this.baseImage = base;
            this.overlayImage = overlay;
        }

        @Override
        public boolean equals(Object other) {
            if (!(other instanceof CompositeImageKey)) {
                return false;
            }

            CompositeImageKey k = (CompositeImageKey) other;

            return (x == k.x) && (y == k.y) && (baseImage == k.baseImage) && (overlayImage == k.overlayImage);
        }

        @Override
        public int hashCode() {
            int hash = ((x << 3) ^ y) << 4;
            hash = hash ^ System.identityHashCode(baseImage) ^ System.identityHashCode(overlayImage);

            return hash;
        }

        @Override
        public String toString() {
            return "Composite key for " + baseImage + " + " + overlayImage + " at [" + x + ", " + y + "]"; // NOI18N
        }
    }
    
    /**
     * Key used for ToolTippedImage
     */
    private static class ToolTipImageKey {
        Image image;
        String str;

        ToolTipImageKey(Image image, String str) {
            this.image = image;
            this.str = str;
        }

        @Override
        public boolean equals(Object other) {
            if (!(other instanceof ToolTipImageKey)) {
                return false;
            }
            ToolTipImageKey k = (ToolTipImageKey) other;
            return (str.equals(k.str)) && (image == k.image);
        }

        @Override
        public int hashCode() {
            return System.identityHashCode(image) ^ str.hashCode();
        }

        @Override
        public String toString() {
            return "ImageStringKey for " + image + " + " + str; // NOI18N
        }
    }

    /** Cleaning reference. */
    private static final class ActiveRef<T> extends SoftReference<ToolTipImage> implements Runnable {
        private final Map<T,ActiveRef<T>> holder;
        private final T key;

        public ActiveRef(ToolTipImage o, Map<T,ActiveRef<T>> holder, T key) {
            super(o, Utilities.activeReferenceQueue());
            this.holder = holder;
            this.key = key;
        }

        public void run() {
            synchronized (holder) {
                holder.remove(key);
            }
        }
    }
     // end of ActiveRef

    /**
     * Wraps an arbitrary {@link Icon} inside an {@link ImageIcon}. This allows us to provide
     * scalable icons from {@link #loadImageIcon(String,boolean)} without changing the API.
     */
    private static final class IconImageIcon extends ImageIcon {
        private final Icon delegate;

        private IconImageIcon(Icon delegate) {
            super(icon2Image(delegate));
            Parameters.notNull("delegate", delegate);
            this.delegate = delegate;
        }

        private static ImageIcon create(Icon delegate) {
            return (delegate instanceof ImageIcon)
                    ? (ImageIcon) delegate : new IconImageIcon(delegate);
        }

        @Override
        public synchronized void paintIcon(Component c, Graphics g, int x, int y) {
            delegate.paintIcon(c, g, x, y);
        }

        public Icon getDelegateIcon() {
            return delegate;
        }
    }

    /**
     * Image with tool tip text (for icons with badges)
     */
    private static class ToolTipImage extends BufferedImage implements Icon {
        final String toolTipText;
        // May be null.
        final Icon delegateIcon;
        final URL url;
        // May be null.
        ImageIcon imageIconVersion;

        public static ToolTipImage createNew(String toolTipText, Image image, URL url) {
            ImageUtilities.ensureLoaded(image);
            boolean bitmask = (image instanceof Transparency) && ((Transparency) image).getTransparency() != Transparency.TRANSLUCENT;
            ColorModel model = colorModel(bitmask ? Transparency.BITMASK : Transparency.TRANSLUCENT);
            int w = image.getWidth(null);
            int h = image.getHeight(null);
            if (url == null) {
                Object value = image.getProperty("url", null);
                url = (value instanceof URL) ? (URL) value : null;
            }            
            Icon icon = (image instanceof ToolTipImage)
                    ? ((ToolTipImage) image).getDelegateIcon() : null;
            ToolTipImage newImage = new ToolTipImage(
                toolTipText,
                icon,
                model,
                model.createCompatibleWritableRaster(w, h),
                model.isAlphaPremultiplied(), null, url
            );

            java.awt.Graphics g = newImage.createGraphics();
            g.drawImage(image, 0, 0, null);
            g.dispose();
            return newImage;
        }

        public ToolTipImage(
            String toolTipText, Icon delegateIcon, ColorModel cm, WritableRaster raster,
            boolean isRasterPremultiplied, Hashtable<?, ?> properties, URL url
        ) {
            super(cm, raster, isRasterPremultiplied, properties);
            this.toolTipText = toolTipText;
            this.delegateIcon = delegateIcon;
            this.url = url;
        }

        public synchronized ImageIcon asImageIcon() {
          if (imageIconVersion == null)
            imageIconVersion = IconImageIcon.create(this);
          return imageIconVersion;
        }

        public ToolTipImage(Icon delegateIcon, String toolTipText, int imageType) {
            // BufferedImage must have width/height > 0.
            super(Math.max(1, delegateIcon.getIconWidth()),
                    Math.max(1, delegateIcon.getIconHeight()), imageType);
            this.delegateIcon = delegateIcon;
            this.toolTipText = toolTipText;
            this.url = null;
        }

        /**
         * Get an {@link Icon} instance representing a scalable version of this {@code Image}.
         *
         * @return may be null
         */
        public Icon getDelegateIcon() {
            return delegateIcon;
        }

        public int getIconHeight() {
            return super.getHeight();
        }

        public int getIconWidth() {
            return super.getWidth();
        }

        public void paintIcon(Component c, Graphics g, int x, int y) {
            if (delegateIcon != null) {
                delegateIcon.paintIcon(c, g, x, y);
            } else {
                /* There is no scalable delegate icon available. On HiDPI displays, this means that
                original low-resolution icons will need to be scaled up to a higher resolution. Do a
                few tricks here to improve the quality of the scaling. See NETBEANS-2614 and the
                before/after screenshots that are attached to said JIRA ticket. */
                Graphics2D g2 = (Graphics2D) g.create();
                try {
                    final AffineTransform tx = g2.getTransform();
                    final int txType = tx.getType();
                    final double scale;
                    if (txType == AffineTransform.TYPE_UNIFORM_SCALE ||
                        txType == (AffineTransform.TYPE_UNIFORM_SCALE | AffineTransform.TYPE_TRANSLATION))
                    {
                      scale = tx.getScaleX();
                    } else {
                      scale = 1.0;
                    }
                    if (scale != 1.0) {
                        /* The default interpolation mode is nearest neighbor. Use bicubic
                        interpolation instead, which looks better, especially with non-integral
                        HiDPI scaling factors (e.g. 150%). Even for an integral 2x scaling factor
                        (used by all Retina displays on MacOS), the blurred appearance of bicubic
                        scaling ends up looking better on HiDPI displays than the blocky appearance
                        of nearest neighbor. */
                        g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
                        g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
                        g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
                        /* For non-integral scaling factors, we frequently encounter non-integral
                        device pixel positions. For instance, with a 150% scaling factor, the
                        logical pixel position (7,0) would map to device pixel position (10.5,0).
                        On such scaling factors, icons look a lot better if we round the (x,y)
                        translation to an integral number of device pixels before painting. */
                        g2.setTransform(new AffineTransform(scale, 0, 0, scale,
                                (int) tx.getTranslateX(), (int) tx.getTranslateY()));
                    }
                    g2.drawImage(this, x, y, null);
                } finally {
                    g2.dispose();
                }
            }
        }

        @Override
        public Object getProperty(String name, ImageObserver observer) {
            if ("url".equals(name)) { // NOI18N
                /* In some cases it might strictly be more appropriate to return
                Image.UndefinedProperty rather than null (see Javadoc spec for this method), but
                retain the existing behavior and use null instead here. That way there won't be a
                ClassCastException if someone tries to cast to URL. */
                if (url != null) {
                    return url;
                } else if (!(delegateIcon instanceof ImageIcon)) {
                    return null;
                } else {
                    Image image = ((ImageIcon) delegateIcon).getImage();
                    if (image == this || image == null) {
                        return null;
                    }
                    return image.getProperty("url", observer);
                }
            }
            return super.getProperty(name, observer);
        }
    }

    private static class DisabledButtonFilter extends RGBImageFilter {
        public static final RGBImageFilter INSTANCE = new DisabledButtonFilter();

        DisabledButtonFilter() {
            canFilterIndexColorModel = true;
        }

        public int filterRGB(int x, int y, int rgb) {
            // Reduce the color bandwidth in quarter (>> 2) and Shift 0x88.
            return (rgb & 0xff000000) + 0x888888 + ((((rgb >> 16) & 0xff) >> 2) << 16) + ((((rgb >> 8) & 0xff) >> 2) << 8) + (((rgb) & 0xff) >> 2);
        }

        // override the superclass behaviour to not pollute
        // the heap with useless properties strings. Saves tens of KBs
        @Override
        public void setProperties(Hashtable props) {
            props = (Hashtable) props.clone();
            consumer.setProperties(props);
        }
    }
}
