/*******************************************************************************
 * Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *     Steven Ketcham (sketcham@dsicdi.com) - Bug 42451
 *     [Dialogs] ImageRegistry throws null pointer exception in
 *     application with multiple Display's
 *******************************************************************************/
package org.eclipse.jface.resource;

import java.util.HashMap;
import java.util.Map;

import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.widgets.Display;

/**
 * An image registry maintains a mapping between symbolic image names
 * and SWT image objects or special image descriptor objects which
 * defer the creation of SWT image objects until they are needed.
 * <p>
 * An image registry owns all of the image objects registered
 * with it, and automatically disposes of them when the SWT Display
 * that creates the images is disposed. Because of this, clients do not
 * need to (indeed, must not attempt to) dispose of these images themselves.
 * </p>
 * <p>
 * Clients may instantiate this class (it was not designed to be subclassed).
 * </p>
 * <p>
 * Unlike the FontRegistry, it is an error to replace images. As a result
 * there are no events that fire when values are changed in the registry
 * </p>
 * @noextend This class is not intended to be subclassed by clients.
 */
public class ImageRegistry {
    /**
     * display used when getting images
     */
    private Display display;

    private ResourceManager manager;

    private Map<String, Entry> table;

	private Runnable disposeRunnable = this::dispose;

    /**
     * Contains the data for an entry in the registry.
     */
    private static class Entry {
    	/** the image */
        protected Image image;

        /** the descriptor */
        protected ImageDescriptor descriptor;
    }

    private static class OriginalImageDescriptor extends ImageDescriptor {
        private Image original;
        private int refCount = 0;
        private Device originalDisplay;

        /**
         * @param original the original image
         * @param originalDisplay the device the image is part of
         */
        public OriginalImageDescriptor(Image original, Device originalDisplay) {
            this.original = original;
            this.originalDisplay = originalDisplay;
        }

        @Override
		public Object createResource(Device device) throws DeviceResourceException {
            if (device == originalDisplay) {
                refCount++;
                return original;
            }
            return super.createResource(device);
        }

        @Override
		public void destroyResource(Object toDispose) {
            if (original == toDispose) {
                refCount--;
                if (refCount == 0) {
                    original.dispose();
                    original = null;
                }
            } else {
                super.destroyResource(toDispose);
            }
        }

        @Override
		public ImageData getImageData(int zoom) {
			return original.getImageData(zoom);
        }
    }

    /**
     * Creates an empty image registry.
     * <p>
     * There must be an SWT Display created in the current
     * thread before calling this method.
     * </p>
     */
    public ImageRegistry() {
        this(Display.getCurrent());
    }

    /**
     * Creates an empty image registry using the given resource manager to allocate images.
     *
     * @param manager the resource manager used to allocate images
     *
     * @since 3.1
     */
    public ImageRegistry(ResourceManager manager) {
        Assert.isNotNull(manager);
        Device dev = manager.getDevice();
        if (dev instanceof Display) {
            this.display = (Display)dev;
        }
        this.manager = manager;
        manager.disposeExec(disposeRunnable);
    }

    /**
     * Creates an empty image registry.
     *
     * @param display this <code>Display</code> must not be
     *        <code>null</code> and must not be disposed in order
     *        to use this registry
     */
    public ImageRegistry(Display display) {
        this(JFaceResources.getResources(display));
    }

    /**
     * Returns the image associated with the given key in this registry,
     * or <code>null</code> if none.
     *
     * @param key the key
     * @return the image, or <code>null</code> if none
     */
    public Image get(String key) {

        // can be null
        if (key == null) {
            return null;
        }

        if (display != null) {
            /**
             * NOTE, for backwards compatibility the following images are supported
             * here, they should never be disposed, hence we explicitly return them
             * rather then registering images that SWT will dispose.
             *
             * Applications should go direclty to SWT for these icons.
             *
             * @see Display.getSystemIcon(int ID)
             */
            int swtKey = -1;
            if (key.equals(Dialog.DLG_IMG_INFO)) {
                swtKey = SWT.ICON_INFORMATION;
            }
            if (key.equals(Dialog.DLG_IMG_QUESTION)) {
                swtKey = SWT.ICON_QUESTION;
            }
            if (key.equals(Dialog.DLG_IMG_WARNING)) {
                swtKey = SWT.ICON_WARNING;
            }
            if (key.equals(Dialog.DLG_IMG_ERROR)) {
                swtKey = SWT.ICON_ERROR;
            }
            // if we actually just want to return an SWT image do so without
            // looking in the registry
            if (swtKey != -1) {
                final Image[] image = new Image[1];
                final int id = swtKey;
                display.syncExec(() -> image[0] = display.getSystemImage(id));
                return image[0];
            }
        }

        Entry entry = getEntry(key);
        if (entry == null) {
            return null;
        }

        if (entry.image == null) {
            entry.image = manager.createImageWithDefault(entry.descriptor);
        }

        return entry.image;
    }

    /**
     * Returns the descriptor associated with the given key in this registry,
     * or <code>null</code> if none.
     *
     * @param key the key
     * @return the descriptor, or <code>null</code> if none
     * @since 2.1
     */
    public ImageDescriptor getDescriptor(String key) {
        Entry entry = getEntry(key);
        if (entry == null) {
            return null;
        }

        return entry.descriptor;
    }

    /**
     * Adds (or replaces) an image descriptor to this registry. The first time
     * this new entry is retrieved, the image descriptor's image will be computed
     * (via </code>ImageDescriptor.createImage</code>) and remembered.
     * This method replaces an existing image descriptor associated with the
     * given key, but fails if there is a real image associated with it.
     *
     * @param key the key
     * @param descriptor the ImageDescriptor
     * @exception IllegalArgumentException if the key already exists
     */
    public void put(String key, ImageDescriptor descriptor) {
        Entry entry = getEntry(key);
        if (entry == null) {
            entry = new Entry();
            getTable().put(key, entry);
        }

        if (entry.image != null) {
            throw new IllegalArgumentException(
                    "ImageRegistry key already in use: " + key); //$NON-NLS-1$
        }

        entry.descriptor = descriptor;
    }

    /**
     * Adds an image to this registry.  This method fails if there
     * is already an image or descriptor for the given key.
     * <p>
     * Note that an image registry owns all of the image objects registered
     * with it, and automatically disposes of them when the SWT Display is disposed.
     * Because of this, clients must not register an image object
     * that is managed by another object.
     * </p>
     *
     * @param key the key
     * @param image the image, should not be <code>null</code>
     * @exception IllegalArgumentException if the key already exists
     */
    public void put(String key, Image image) {
        Entry entry = getEntry(key);

        if (entry == null) {
            entry = new Entry();
            putEntry(key, entry);
        }

        if (entry.image != null || entry.descriptor != null) {
            throw new IllegalArgumentException(
                    "ImageRegistry key already in use: " + key); //$NON-NLS-1$
        }

        // Check for a null image here, otherwise the problem won't appear
        // until dispose.
        // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=130315
        Assert.isNotNull(image, "Cannot register a null image."); //$NON-NLS-1$
        entry.image = image;
        entry.descriptor = new OriginalImageDescriptor(image, manager.getDevice());

        try {
            manager.create(entry.descriptor);
        } catch (DeviceResourceException e) {
        }
    }

    /**
     * Removes an image from this registry.
     * If an SWT image was allocated, it is disposed.
     * This method has no effect if there is no image or descriptor for the given key.
     * @param key the key
     */
    public void remove(String key) {
        ImageDescriptor descriptor = getDescriptor(key);
        if (descriptor != null) {
            manager.destroy(descriptor);
            getTable().remove(key);
        }
    }

    private Entry getEntry(String key) {
        return getTable().get(key);
    }

    private void putEntry(String key, Entry entry) {
        getTable().put(key, entry);
    }

    private Map<String, Entry> getTable() {
        if (table == null) {
            table = new HashMap<>(10);
        }
        return table;
    }

    /**
     * Disposes this image registry, disposing any images
     * that were allocated for it, and clearing its entries.
     *
     * @since 3.1
     */
    public void dispose() {
        manager.cancelDisposeExec(disposeRunnable);

        if (table != null) {
            for (Entry entry : table.values()) {
                if (entry.image != null) {
                    manager.destroyImage(entry.descriptor);
                }
            }
            table = null;
        }
        display = null;
    }
}
