package org.limewire.ui.swing.images;

import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.Callable;

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.JComponent;
import javax.swing.JList;
import javax.swing.SwingUtilities;

import org.limewire.ui.swing.util.GraphicsUtilities;
import org.limewire.util.FileUtils;

/**
 * Loads an image and creates a thumbnail of it on the ImageExceturService thread. 
 * If an error occurs while reading the file and error Image will be loaded in
 * its place. If a component is passed in, it will be treated as a callback to
 * refresh the component after the thumbnail has been created.
 * <p>
 * If a component is passed in expecting a callback, the component must still be
 * visible when the thumbnail is set to be created. If the component is a list
 * that is expecting a callback, the index for the thumbnail must still be 
 * visible prior to the thumbnail being loaded.
 */
public class ThumbnailCallable implements Callable<Void> {
    
    private final Map<File,Icon> thumbnailMap;
    private final Map<File,String> loadingMap;
    private File file;
    private final Icon errorIcon;
    private final JComponent callback;
    
    private int index = -1;
    
    /**
     * Reads the image file and create a thumbnail, storing the thumbnail
     * in the thumbnail map. If an error occurs, store the error icon instead.
     * 
     * @param thumbnailMap map to store the thumbnail in
     * @param loadingMap map of files waiting to be loaded as thumbnails
     * @param file image file to read and create a thumbnail from
     * @param errorIcon icon to show if the file can't be read
     */
    public ThumbnailCallable(Map<File,Icon> thumbnailMap, Map<File,String> loadingMap, File file, Icon errorIcon) {
        this(thumbnailMap, loadingMap, file, errorIcon, null);
    }
    
    /**
     * Reads the image file and create a thumbnail, storing the thumbnail
     * in the thumbnail map. If an error occurs, store the error icon instead.
     * Call a repaint on the component once the thumbnail has been created. If
     * the component is no longer showing don't bother repainting it.
     * <p>
     * If the callback is no longer visible when the load method is created, the
     * thumbnail is not created. 
     * 
     * @param thumbnailMap map to store the thumbnail in
     * @param loadingMap map of files waiting to be loaded as thumbnails
     * @param file image file to read and create a thumbnail from
     * @param errorIcon icon to show if the file can't be read
     * @param callback component to repaint once the thumbnail has been created.
     */
    public ThumbnailCallable(Map<File,Icon> thumbnailMap, Map<File,String> loadingMap, File file, Icon errorIcon, JComponent callback) {
        this.thumbnailMap = thumbnailMap;
        this.loadingMap = loadingMap;
        this.file = file;
        this.errorIcon = errorIcon;
        this.callback = callback;
    }
    
    /**
     * Reads the image file and create a thumbnail, storing the thumbnail
     * in the thumbnail map. If an error occurs, store the error icon instead.
     * Call a repaint on the component once the thumbnail has been created. If
     * the component is no longer showing don't bother repainting it.
     * <p>
     * If the callback is no longer visible when the load method is created, the
     * thumbnail is not created. If the list is still visible but the index is 
     * no longer shown in the list, the image is not loaded. 
     * 
     * @param thumbnailMap map to store the thumbnail in
     * @param loadingMap map of files waiting to be loaded as thumbnails
     * @param file image file to read and create a thumbnail from
     * @param errorIcon icon to show if the file can't be read
     * @param callback component to repaint once the thumbnail has been created.
     * @param index index within this list, this thumbnail is intended for
     */
    public ThumbnailCallable(Map<File,Icon> thumbnailMap, Map<File,String> loadingMap, File file, Icon errorIcon, JList list, int index) {
        this.thumbnailMap = thumbnailMap;
        this.loadingMap = loadingMap;
        this.file = file;
        this.errorIcon = errorIcon;
        this.callback = list;
        this.index = index;
    }
    
    @Override
    public Void call() throws Exception {
        // do some testing. If the component this thumbnail is intended for isn't showing anymore,
        // don't waste the time loading the thumbnail
        if(callback != null) {
            if(!callback.isShowing()) {
                // thumbnail wasn't loaded, remove it from the list of pending thumbnails
                loadingMap.remove(file);
                return null;
            }
            if((callback instanceof JList) && index != -1 && (((JList)callback).getFirstVisibleIndex() > index || ((JList)callback).getLastVisibleIndex() < index)){
                // thumbnail wasn't loaded, remove it from the list of pending thumbnails
                loadingMap.remove(file);
                return null;
            }
        }
        BufferedImage image = null;
        try {  
            image = getSubSampleImage(file);
        } catch (Throwable e) {
            try {
                image = ImageIO.read(file);
            } catch(Throwable ee) {
                handleUpdate(errorIcon);
                return null;
            }
        }  
        if(image == null) {
            handleUpdate(errorIcon);
            return null;
        }
        // if the image is larger than our viewport, resize the image before saving
        if(image.getWidth() > ThumbnailManager.WIDTH || image.getHeight() > ThumbnailManager.HEIGHT) { 
            // image manipulation can cause a whole host of errors, it should always be wrapped in a try/catch block
            try { 
                image = GraphicsUtilities.createRatioPreservedThumbnail(image, ThumbnailManager.WIDTH, ThumbnailManager.HEIGHT);
            } catch(Throwable t) {
                try { // if there was an error, try creating a less detailed thumbnail
                    image = GraphicsUtilities.createRatioPreservedThumbnailFast(image, ThumbnailManager.WIDTH, ThumbnailManager.HEIGHT);
                } catch (Throwable e) { // give up
                    image = null;
                }
            }
        } else {
            // if the image didn't need to be scaled, make sure it can be accelerated
            image = GraphicsUtilities.toCompatibleImage(image);
        }
        
        if(image == null || image.getWidth() == 0 || image.getHeight() == 0) {
            handleUpdate(errorIcon);
            return null;
        }
        ImageIcon imageIcon = new ImageIcon(image);
        handleUpdate(imageIcon);
        
        return null;
    }
    
    /**
     * Store the image in a hashmap. If a component callback was passed in, 
     * see if the component is still showing and if so call a repaint on it.
     */
    private void handleUpdate(Icon icon) {
        thumbnailMap.put(file, icon);
        loadingMap.remove(file);
        if(callback != null && callback.isShowing()) {
            SwingUtilities.invokeLater(new Runnable(){
                @Override
				public void run() {
                    callback.repaint();                    
                }
            });
        }
    }
    
    private static final float subSamplingFactor = ThumbnailManager.WIDTH * 4.20f;
    
    /**
     * Loads a file into a BufferedImage. If the image is longer than a subSamplingFactor, 
     * rows are sampled out to reduce the size of the BufferedImage. This can go a long way
     * towards reducing OutOfMemoryExceptions when loading large compressed images.
     */
    private BufferedImage getSubSampleImage(File file) throws FileNotFoundException, IOException {
        
        BufferedImage image;
        ImageInputStream bufferedInput = null;
        
        try {
            final ImageReader imgReader = ImageIO.getImageReadersBySuffix( FileUtils.getFileExtension(file) ).next(); 
            bufferedInput = ImageIO.createImageInputStream( new BufferedInputStream( new FileInputStream( file ) ) ); 
            
            imgReader.setInput(bufferedInput); 
            
            int imgHeight = imgReader.getHeight( 0 );
            int imgWidth = imgReader.getWidth( 0 ); 
    
            int longEdge = (Math.max(imgHeight, imgWidth));
            int subSample = (int)(longEdge/subSamplingFactor); 
     
            final ImageReadParam readParam = imgReader.getDefaultReadParam();
            if(subSample > 1) {
                readParam.setSourceSubsampling(subSample, subSample, 0, 0);
            } 
            image = imgReader.read(0, readParam);
        } finally {
            if(bufferedInput != null)
                bufferedInput.close();
        }
        return image; 
    }
}
