package prefuse.render;

import java.awt.Component;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;

import prefuse.data.Tuple;
import prefuse.util.io.IOLib;


/**
 * <p>Utility class that manages loading and storing images. Includes a
 * configurable LRU cache for managing loaded images. Also supports optional
 * image scaling of loaded images to cut down on memory and visualization
 * operation costs.</p>
 * 
 * <p>By default images are loaded upon first request. Use the
 * {@link #preloadImages(Iterator, String)} method to load images before they
 * are requested for rendering.</p>
 * 
 * @author alan newberger
 * @author <a href="http://jheer.org">jeffrey heer</a>
 */
public class ImageFactory {
    
    protected int m_imageCacheSize = 3000;
    protected int m_maxImageWidth  = 100;
    protected int m_maxImageHeight = 100;
    protected boolean m_asynch = true;
    
    //a nice LRU cache courtesy of java 1.4
    protected Map imageCache =
        new LinkedHashMap((int) (m_imageCacheSize + 1 / .75F), .75F, true) {
            public boolean removeEldestEntry(Map.Entry eldest) {
                return size() > m_imageCacheSize;
            }
        };
    protected Map loadMap = new HashMap(50);

    protected final Component component = new Component() {};
    protected final MediaTracker tracker = new MediaTracker(component);
    protected int nextTrackerID = 0;

    /**
     * Create a new ImageFactory. Assumes no scaling of loaded images.
     */
    public ImageFactory() {
        this(-1,-1);
    }
    
    /**
     * Create a new ImageFactory. This instance will scale loaded images
     * if they exceed the threshold arguments.
     * @param maxImageWidth the maximum width of input images
     *  (-1 means no limit)
     * @param maxImageHeight the maximum height of input images
     *  (-1 means no limit)
     */
    public ImageFactory(int maxImageWidth, int maxImageHeight) {
        setMaxImageDimensions(maxImageWidth, maxImageHeight);
    }

    /**
     * Sets the maximum image dimensions of loaded images, images larger than
     * these limits will be scaled to fit within bounds.
     * @param width the maximum width of input images (-1 means no limit)
     * @param height the maximum height of input images (-1 means no limit)
     */
    public void setMaxImageDimensions(int width, int height) {
        m_maxImageWidth  = width;
        m_maxImageHeight = height;
    }

    /**
     * Sets the capacity of this factory's image cache
     * @param size the new size of the image cache
     */
    public void setImageCacheSize(int size) {
        m_imageCacheSize = size;
    }

    /**
     * Indicates if the given string location corresponds to an image
     * currently stored in this ImageFactory's cache.
     * @param imageLocation the image location string
     * @return true if the location is a key for a currently cached image,
     * false otherwise.
     */
    public boolean isInCache(String imageLocation) {
    	return imageCache.containsKey(imageLocation);
    }
    
    /**
     * <p>Get the image associated with the given location string. If the image
     * has already been loaded, it simply will return the image, otherwise it
     * will load it from the specified location.</p>
     * 
     * <p>The imageLocation argument must be a valid resource string pointing
     * to either (a) a valid URL, (b) a file on the classpath, or (c) a file
     * on the local filesystem. The location will be resolved in that order.
     * </p>
     * 
     * @param imageLocation the image location as a resource string.
     * @return the corresponding image, if available
     */
    public Image getImage(String imageLocation) {
        Image image = (Image) imageCache.get(imageLocation);
        if (image == null && !loadMap.containsKey(imageLocation)) {
            URL imageURL = IOLib.urlFromString(imageLocation); 
            if ( imageURL == null ) {
                System.err.println("Null image: " + imageLocation);
                return null;
            }
            image = Toolkit.getDefaultToolkit().createImage(imageURL);
            
            // if set for synchronous mode, block for image to load.
            if ( !m_asynch ) {
                waitForImage(image);
                addImage(imageLocation, image);
            } else {
                int id = ++nextTrackerID;
                tracker.addImage(image, id);
                loadMap.put(imageLocation, new LoadMapEntry(id,image));    
            }
        } else if ( image == null && loadMap.containsKey(imageLocation) ) {
            LoadMapEntry entry = (LoadMapEntry)loadMap.get(imageLocation);
            if ( tracker.checkID(entry.id, true) ) {
                addImage(imageLocation, entry.image);
                loadMap.remove(imageLocation);
                tracker.removeImage(entry.image, entry.id);
            }
        } else {
            return image;
        }
        return (Image) imageCache.get(imageLocation);
    }
    
    /**
     * Adds an image associated with a location string to this factory's cache.
     * The image will be scaled as dictated by this current factory settings.
     * 
     * @param location the location string uniquely identifying the image
     * @param image the actual image
     * @return the final image added to the cache. This may be a scaled version
     *         of the original input image.
     */
    public Image addImage(String location, Image image) {
        if ( m_maxImageWidth > -1 || m_maxImageHeight > -1 ) {
            image = getScaledImage(image);
            image.getWidth(null); // trigger image load
        }
        imageCache.put(location, image);
        return image;
    }
    
    /**
     * Wait for an image to load.
     * @param image the image to wait for
     */
    protected void waitForImage(Image image) {
        int id = ++nextTrackerID;
        tracker.addImage(image, id);
        try {
            tracker.waitForID(id, 0);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tracker.removeImage(image, id);
    }
    
    /**
     * Scales an image to fit within the current size thresholds.
     * @param img the image to scale
     * @return the scaled image
     */
    protected Image getScaledImage(Image img) {     
        // resize image, if necessary, to conserve memory
        //  and reduce future scaling time
        int w = img.getWidth(null) - m_maxImageWidth;
        int h = img.getHeight(null) - m_maxImageHeight;

        if ( w > h && w > 0 && m_maxImageWidth > -1 ) {
            Image scaled = img.getScaledInstance(m_maxImageWidth, -1, Image.SCALE_SMOOTH);
            img.flush(); //waitForImage(scaled);
            return scaled;
        } else if ( h > 0 && m_maxImageHeight > -1 ) {
            Image scaled = img.getScaledInstance(-1, m_maxImageHeight, Image.SCALE_SMOOTH);
            img.flush(); //waitForImage(scaled);                
            return scaled;
        } else {
            return img;
        }
    }
    
    /**
     * <p>Preloads images for use in a visualization. Images to load are
     * determined by taking objects from the given iterator and retrieving
     * the value of the specified field. The items in the iterator must
     * be instances of the {@link prefuse.data.Tuple} class.</p>
     * 
     * <p>Images are loaded in the order specified by the iterator until the
     * the iterator is empty or the maximum image cache size is met. Thus
     * higher priority images should appear sooner in the iteration.</p>
     * 
     * @param iter an Iterator of {@link prefuse.data.Tuple} instances
     * @param field the data field that contains the image location
     */
    public void preloadImages(Iterator iter, String field) {
        boolean synch = m_asynch;
        m_asynch = false;
        
        String loc = null;
        while ( iter.hasNext() && imageCache.size() <= m_imageCacheSize ) {
            // get the string describing the image location
            Tuple t = (Tuple)iter.next();
            loc = t.getString(field);
            if ( loc != null ) {
                getImage(loc);
            }
        }
        m_asynch = synch;
    }
    
    /**
     * Helper class for storing an id/image pair.
     */
    private class LoadMapEntry {
        public int id;
        public Image image;
        public LoadMapEntry(int id, Image image) {
            this.id = id;
            this.image = image;
        }
    }
    
} // end of class ImageFactory
