package org.limewire.ui.swing.util;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFileChooser;
import javax.swing.filechooser.FileSystemView;
import javax.swing.filechooser.FileView;
import javax.swing.plaf.FileChooserUI;

import org.limewire.collection.FixedsizeForgetfulHashMap;
import org.limewire.collection.FixedsizeForgetfulHashSet;
import org.limewire.concurrent.ExecutorsHelper;
import org.limewire.core.api.Category;
import org.limewire.core.api.file.CategoryManager;
import org.limewire.core.settings.SharingSettings;
import org.limewire.ui.swing.settings.SwingUiSettings;
import org.limewire.util.FileUtils;
import org.limewire.util.OSUtils;
import org.limewire.util.SystemUtils;
import org.limewire.util.SystemUtils.SpecialLocations;

import com.google.inject.Inject;


/** A FileIconController that attempts to return native icons. */
public class NativeFileIconController implements FileIconController {
    
    /** The view that retrieves the icon from the filesystem. */
    private final SmartFileView VIEW;
    
    /** A mapping from String (extension) to Icon. */
    private final Map<String, Icon> EXTENSIONS = new HashMap<String, Icon>();
    
    /**
     * A marker null icon so we don't create a file everytime
     * if the icon can't be found.
     */
    private final Icon NULL = new ImageIcon();
    
    private final CategoryManager categoryManager;
    
    /**
     * Constructs the NativeFileIconController.
     * This constructor may block as the JFileChooser
     * is constructed.
     */
    @Inject
    NativeFileIconController(CategoryManager categoryManager) {
        this.categoryManager = categoryManager;
        SmartFileView view = getNativeFileView();
        if(view == null) {
            VIEW = null;
        } else {
            VIEW = new DelegateFileView(view);
            if(SwingUiSettings.PRELOAD_NATIVE_ICONS.getValue())
                preload();
        }
    }
    
    /** Returns true if loading succeeded and the view hasn't screwed up. */
    @Override
	public boolean isValid() {
        return VIEW != null && VIEW.isViewAvailable();
    }
    
    /** Returns true if we have requested this file recently. */
    @Override
	public boolean isIconForFileAvailable(File f) {
        return VIEW.isIconCached(f);
    }
    
    /**
     * Retrieves the native FileView.
     */
    private SmartFileView getNativeFileView() {
        // Deadlocks happen on Windows when using file-chooser based view.
        if(OSUtils.isWindows())
            return constructFSVView();
        else 
            return constructFileChooserView();
    }
    
    /**
     * Constructs a JFileChooser-based FileView.
     * This doesn't work consistently on Java 1.5.0_10 on Windows,
     * because it deadlocks if called outside of the Swing thread.
     * @return
     */
    private SmartFileView constructFileChooserView() {
        // This roundabout way of getting the FileView is necessary for the
        // following reasons:
        // 1) We need the native UI's FileView to get the correct icons,
        //    because the Metal UI's icons are terrible.
        // 2) We cannot just call getFileView(chooser) once retrieving the
        //    native UI, because FileChooserUI tends to delegate calls
        //    to the JFileChooser, and it seems to require that it
        //    the UI be set on the chooser.
        // 3) setUI is a protected method of JFileChooser (of JComponent),
        //    so we need to have the anonymous class with an extended
        //    constructor.
        // 4) Even after constructing the JFileChooser, using getIcon on it
        //    doesn't work well, so we need to do it directly on the FileView.
        // 5) In order to get the correct file view, it needs to be explicitly
        //    set, otherwise it reverts to the UI's FileView, using UIManager,
        //    which may actually be a different UI.
        // 6) The NullPointerException must be caught because sometimes
        //    the Windows JFileChooser throws an NPE while constructing.
        //    The ArrayIndexOutOfBoundsException is a workaround also,
        //    as that error seems to be thrown occasionally in Java 1.6.
        
        JFileChooser chooser = null;        
        // If after 10 times we still can't set it just give up.
        for(int i = 0; i < 10; i++) {
            try {
                chooser = new JFileChooser() {
                    {
                        FileChooserUI ui =
                            (FileChooserUI)GuiUtils.getNativeUI(this);
                        setUI(ui);
                        setFileView(ui.getFileView(this));
                    }
                };
            } catch(NullPointerException ignored) {
            } catch(ArrayIndexOutOfBoundsException ignored) {
            }
            
            if(chooser != null)
                break;
        }
        
        if(chooser == null) {
            return null;
        } else {
            return new SmartChooserView(chooser.getFileView());
        }        
    }
    
    /**
     * Constructs a FileSystemView-based FileView.
     * Just to be safe, we do this on the Swing thread, since we've seen
     * deadlocks when constructing JFileChooser ones outside the Swing thread.
     */
    public SmartFileView constructFSVView() {
        final AtomicReference<SmartFileView> ref = new AtomicReference<SmartFileView>();
        SwingUtils.invokeNowOrWait(new Runnable() {
            @Override
			public void run() {
                try {
                    ref.set(new FSVFileView());
                } catch(Throwable err) {
                    // If an error constructing FSF view, ignore.
                    ref.set(null);
                }
            }
        });
        return ref.get();
    }
    
    /**
     * Returns the native file icon for a file, if it exists.
     * If it doesn't exist, returns the icon for the extension of the file.
     */
    @Override
	public Icon getIconForFile(File f) {
        if (f == null)
            return null;

        // We return the icon if it was previously cached,
        // or if it exists.  We cannot get a nonexistant (non-cached)
        // file from the view, otherwise they'll be spurious exceptions.
        if(VIEW.isIconCached(f) || f.exists()) {
            return VIEW.getIcon(f);
        } else {
            String extension = FileUtils.getFileExtension(f);
            if (!extension.isEmpty())
                return getIconForExtension(extension);
            else
                return null;
        }
    }
     
    /**
     * Returns the icon associated with the extension.
     */ 
     /* TODO: Implement better.
     */
    @Override
	public Icon getIconForExtension(String ext) {
        ext = ext.trim().toLowerCase();
        Icon icon = EXTENSIONS.get(ext);
        // If we already had a cached icon..
        if(icon != null) {
            // So long as it wasn't the NULL marker,
            // return that icon.
            if(icon != NULL)
                return icon;
            else 
                return null;
        }

        // If we don't know the icon for this extension yet,
        // then create a temporary file, get icon, cache it,
        // and return it.
        File dir = SharingSettings.INCOMPLETE_DIRECTORY.get();
        File tmp = new File(dir, ".LimeWireIconFinder." + ext);
        if(tmp.exists()) {
            icon = VIEW.getIcon(tmp);
        } else {
            try {
                FileUtils.touch(tmp);
                icon = VIEW.getIcon(tmp);
                if(icon == null)
                    icon = NULL;
            } catch(IOException fnfe) {
                icon = NULL;
            }
        }
        
        tmp.delete();
        EXTENSIONS.put(ext, icon);
        VIEW.removeFromCache(tmp); // do not need to keep it around.
        
        return icon;
    }
    
    
    /**
     * Preloads a bunch of icons.
     */
    private void preload() {
        ExecutorService queue = ExecutorsHelper.newProcessingQueue("IconLoader");
        Category[] categories = Category.values();
        final AtomicBoolean continueLoading = new AtomicBoolean(true);
        for(int i = 0; i < categories.length && continueLoading.get(); i++) {
            final Collection<String> exts = categoryManager.getExtensionsForCategory(categories[i]);
            for(Iterator<String> j = exts.iterator(); j.hasNext() && continueLoading.get(); ) {
                final String next = j.next();
                queue.execute(new Runnable() {
                    @Override
					public void run() {
                        SwingUtils.invokeNowOrWait(new Runnable() {
                            @Override
							public void run() {
                                getIconForExtension(next);
                                if(!VIEW.isViewAvailable())
                                    continueLoading.set(false);
                            }
                        });
                    }
                });
            }
        }
    }
    
    /** A smarter FileView. */
    private static abstract class SmartFileView extends FileView {
        
        /** Checks to see if the given icon is cached. */
        public abstract boolean isIconCached(File f);
        
        /** Removes a file from the cache, if possible. */
        public abstract boolean removeFromCache(File f);
        
        /** Determines if this view is working.  By default, returns true. */
        public boolean isViewAvailable() {
            return true;
        }
    }
    
    /**
     * Delegates to another FileView, catching NPEs & UnsatisfiedLinkErrors.
     *
     * The NPE catching is required because of poorly built methods in
     * javax.swing.filechooser.FileSystemView that print true
     * exceptions to System.err and return null, instead of
     * letting the exception propagate.
     * <p>
     * The ULE catching is required because of strange Swing errors
     * that can't find the native code to:
     *  sun.awt.shell.Win32ShellFolder2.getFileSystemPath(I)Ljava/lang/String;     * 
     * See: LWC-1174.
     */
    private static class DelegateFileView extends SmartFileView {
        private final SmartFileView DELEGATE;
        private boolean linkFailed = false;
        
        DelegateFileView(SmartFileView real) {
            DELEGATE = real;
        }
        
        @Override
        public boolean isViewAvailable() {
            return !linkFailed;
        }
        
        @Override
        public Icon getIcon(final File f) {
            try {
                Icon icon = DELEGATE.getIcon(f);
                return icon;
            } catch(NullPointerException npe) {
                return null;
            } catch(UnsatisfiedLinkError ule) {
                linkFailed = true;
                return null;
            }
        }
        
        @Override
        public String getDescription(File f) {
            try {
                return DELEGATE.getDescription(f);
            } catch(NullPointerException npe) {
                return null;
            } catch(UnsatisfiedLinkError ule) {
                linkFailed = true;
                return null;
            }
        }
        
        @Override
        public String getName(File f) {
            try {
                return DELEGATE.getName(f);
            } catch (NullPointerException npe) {
                return null;
            } catch(UnsatisfiedLinkError ule) {
                linkFailed = true;
                return null;
            }
        }

        @Override
        public String getTypeDescription(File f) {
            try {
                return DELEGATE.getTypeDescription(f);
            } catch (NullPointerException npe) {
                return null;
            } catch(UnsatisfiedLinkError ule) {
                linkFailed = true;
                return null;
            }
        }

        @Override
        public Boolean isTraversable(File f) {
            try {
                return DELEGATE.isTraversable(f);
            } catch (NullPointerException npe) {
                return null;
            } catch(UnsatisfiedLinkError ule) {
                linkFailed = true;
                return null;
            }
        }

        @Override
        public boolean isIconCached(File f) {
            return DELEGATE.isIconCached(f);
        }

        /** Does nothing. */
        @Override
        public boolean removeFromCache(File f) {
            return DELEGATE.removeFromCache(f);
        }
    }
    
    /**
     * A FileSystemView FileView.
     */
    private static class FSVFileView extends SmartFileView {
        private final FileSystemView VIEW = FileSystemView.getFileSystemView();        
        private final Map<File, Icon> CACHE = new FixedsizeForgetfulHashMap<File, Icon>(50000);
        
        FSVFileView() {
            // Explicitly put the icon for roots as gotten from their roots description.
            // For some reason, getting the icon for the desktop requires that you pass
            // the File as retrieved from getRoots instead of a file at the desktop's
            // location on FS.
            File[] roots = VIEW.getRoots();
            for(int i = 0; i < roots.length; i++) {
                CACHE.put(new File(roots[i].getPath()), VIEW.getSystemIcon(roots[i]));
            }
            
            // Similarly, with the My Documents folder, the icon is only valid if it's
            // retrieved as the child from the root.
            // Do not execute this code on windows 7. The default logic works fine. 
            // On windows 7 this breaks because View.getRoots only returns one root,
            // the path to the Desktop folder.
            // We bypass this code on Windows Vista.  It fails because the 
            // Documents folder no longer has a desktop icon on Vista.
            if(OSUtils.isWindows() && !OSUtils.isWindowsVista() && !OSUtils.isWindows7() && roots.length == 1) {
                String path = SystemUtils.getSpecialPath(SpecialLocations.DOCUMENTS);
                if(path != null) {
                    File documents = new File(path);
                    File child = VIEW.getChild(roots[0], documents.getName());
                    if(child != null) {
                        CACHE.put(documents, VIEW.getSystemIcon(child));
                    }
                }
            }
        }
        
        @Override
        public String getDescription(File f) {
            return VIEW.getSystemTypeDescription(f);
        }

        @Override
        public Icon getIcon(final File f) {
            Icon icon = CACHE.get(f);
            if(icon == null) {
                icon = VIEW.getSystemIcon(f);
                CACHE.put(f, icon);
            }
            return icon;
        }

        @Override
        public String getName(File f) {
            return VIEW.getSystemDisplayName(f);
        }

        @Override
        public String getTypeDescription(File f) {
            return VIEW.getSystemTypeDescription(f);
        }

        @Override
        public Boolean isTraversable(File f) {
            return VIEW.isTraversable(f);
        }

        @Override
        public boolean isIconCached(File f) {
            return CACHE.containsKey(f);
        }

        /** Removes the given file from the cache. */
        @Override
        public boolean removeFromCache(File f) {
            return CACHE.remove(f) != null;
        }
    }
    
    /**
     * A wrapper around JFileChooser's view that returns true for caching
     * once a file has been looked up.
     */
    private static class SmartChooserView extends SmartFileView {
        /** The view this uses. */
        private final FileView DELEGATE;
        
        /** A set of the most recently requested Files. */
        private final Set<File> CACHE = new FixedsizeForgetfulHashSet<File>(5000, 1000); 

        public SmartChooserView(FileView delegate) {
            DELEGATE = delegate;
        }

        @Override
        public String getDescription(File f) {
            return DELEGATE.getDescription(f);
        }

        @Override
        public Icon getIcon(File f) {
            CACHE.add(f);
            return DELEGATE.getIcon(f);
        }

        @Override
        public String getName(File f) {
            return DELEGATE.getName(f);
        }

        @Override
        public String getTypeDescription(File f) {
            return DELEGATE.getTypeDescription(f);
        }

        @Override
        public Boolean isTraversable(File f) {
            return DELEGATE.isTraversable(f);
        }

        @Override
        public boolean isIconCached(File f) {
            return CACHE.contains(f);
        }

        @Override
        public boolean removeFromCache(File f) {
            return CACHE.remove(f);
        }
    }
}
