package org.limewire.ui.swing.util;

import java.awt.FileDialog;
import java.io.BufferedReader;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.swing.filechooser.FileFilter;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.service.ErrorService;
import org.limewire.ui.swing.components.LimeJFrame;
import org.limewire.util.CommonUtils;
import org.limewire.util.NativeLibraryLoader;
import org.limewire.util.OSUtils;

/**
 * A collection of utility methods for OSX.
 * These methods should only be called if run from OSX,
 * otherwise ClassNotFoundErrors may occur.
 * 
 * Clients may use the method isNativeLibraryLoadedCorrectly() 
 * to check whether the native library loaded correctly.
 * If not, they may choose to disable certain user interface
 * features to reflect this state.
 * 
 * <p>
 * To determine if the Cocoa Foundation classes are present,
 * use the method CommonUtils.isCocoaFoundationAvailable().
 */
public class MacOSXUtils {
    
    /**
     * The application bundle identifier for the LimeWire application that is packed into its Info.plist config file.
     */
    public static final String LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER = "com.wireshare.gnutella";
    
    /**
     * The name of the app that launches.
     */
    private static final String APP_NAME = "WireShare.app";

    private static boolean nativeLibraryLoadedCorrectly = false;
    
    private static final Log LOG = LogFactory.getLog(MacOSXUtils.class);
    
    static {
        if (OSUtils.isMacOSX()) {
            try {
                nativeLibraryLoadedCorrectly = NativeLibraryLoader.loadFirstAvailable(
                        "MacOSXUtils",
                        "lib/native/libMacOSXUtils.dylib",
                        "nativelibs/osx/libMacOSXUtils.dylib",
                        "build/native/osx/libMacOSXUtils.dylib");
            } catch (UnsatisfiedLinkError err) {
                ErrorService.error(err, "java.library.path=" + System.getProperty("java.library.path") + "\n\n" + "trace dependencies=" + MacOSXUtils.traceLibraryDependencies("MacOSXUtils.jnilib"));
            }
        }
    }
    
    /**
     * This returns a boolean indicating whether an exception occurred when loading the native library.
     * @return true if the native library loaded without any errors.
     */
    public static boolean isNativeLibraryLoadedCorrectly() {
        return nativeLibraryLoadedCorrectly;
    }

    private MacOSXUtils() {}
    
    /**
    * If a given library is not loading for some users on OS-X, this method
    * can be used to trace what other libraries this library is dependent
    * on and whether those libraries are present on the user's system.
    */
    public static String traceLibraryDependencies(String libraryName) {       
        StringBuffer traceResultsBuffer = new StringBuffer("ls command output: ");
        String lsCommand = "ls " + System.getProperty("user.dir");
        traceResultsBuffer.append("(").append(lsCommand).append(") "); 
        traceResultsBuffer.append( getCommandOutput(lsCommand) );
        traceResultsBuffer.append( "\n" );

        String otoolCommand = "otool -L " + System.getProperty("user.dir") + "/" + libraryName;
        traceResultsBuffer.append("otool command output: ");
        traceResultsBuffer.append( getCommandOutput(otoolCommand) );
        traceResultsBuffer.append( "\n" );
               
        return traceResultsBuffer.toString();
    }
    
    /**
    * This method runs a system command and returns the command's output as a string.
    *
    */
    private static String getCommandOutput(String command) {
        StringBuffer outputBuffer = new StringBuffer("");

        try {
            // start the command running
            Runtime runtime = Runtime.getRuntime();
            Process process = runtime.exec(command);
    
            // put a BufferedReader on the command output
            InputStream inputstream = process.getInputStream();
            InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
            BufferedReader bufferedreader = new BufferedReader(inputstreamreader);
    
            // read the command output   
            String line;
            while ((line = bufferedreader.readLine()) != null) {
                outputBuffer.append(line).append("\n");
            }
        
            // check for command failure
            try {
                if (process.waitFor() != 0) {
                    outputBuffer.append("exit value = ");
                    outputBuffer.append(process.exitValue());
                }
            }
            catch (InterruptedException e) {
            }
        } catch (IOException exc) {
        }
        
        return outputBuffer.toString();
    }

    
    /**
     * Modifies the loginwindow.plist file to either include or exclude
     * starting up LimeWire.
     */
    public static void setLoginStatus(boolean allow) {
        try {
            SetLoginStatusNative(allow);
        } catch(UnsatisfiedLinkError ule) {
            LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule);
        }
    }
    
    /**
     * Gets the full user's name.
     */
    public static String getUserName() {
        try {
            return GetCurrentFullUserName();
        } catch(UnsatisfiedLinkError ule) {
            // No big deal, just return user name.
            LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule);
            
            return CommonUtils.getUserName();
        }
    }
    
    /**
     * Retrieves the app directory & name.
     * If the user is not running from the bundled app as we named it,
     * defaults to /Applications/WireShare/ as the directory of the app.
     */
    public static String getAppDir() {
        String appDir = "/Applications/WireShare/";
        String path = CommonUtils.getCurrentDirectory().getPath();
        int app = path.indexOf("WireShare.app");
        if(app != -1)
            appDir = path.substring(0, app);
        return appDir + APP_NAME;
    }

    /**
     * This sets LimeWire as the default handler for this file type.
     * @param fileType -- the file extension for the file. this will be used to look up the file type's UTI (universal type identifier)
     */
    public static void setLimewireAsDefaultFileTypeHandler(String fileType) {
        try {
            SetDefaultFileTypeHandler(fileType, LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER);
        } catch(UnsatisfiedLinkError ule) {
            LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule); 
        }
    }

    /**
     * This sets LimeWire as the default handler for this URL scheme.
     * @param url scheme -- the designator for the protocol, e.g. magnet
     */
    public static void setLimewireAsDefaultURLSchemeHandler(String urlScheme) {
        try {
            SetDefaultURLSchemeHandler(urlScheme, LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER);
        } catch(UnsatisfiedLinkError ule) {
            LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule); 
        }
    }

    /**
     * This checks whether LimeWire is the default handler for this file type.
     * @param fileType -- the file extension for the file. this will be used to look up the file type's UTI (universal type identifier)
     * @return true if LimeWire is the default handler for this file type
     */
    public static boolean isLimewireDefaultFileTypeHandler(String fileType) {
        try {
            return IsApplicationTheDefaultFileTypeHandler(fileType, LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER);
        } catch(UnsatisfiedLinkError ule) {
            LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule); 
        }
        
        return true;
    }

    /**
     * This checks whether LimeWire is the default handler for this URL scheme.
     * @param urlScheme -- the designator for the protocol, e.g. magnet
     * @return true if LimeWire is the default handler for this URL scheme
     */
    public static boolean isLimewireDefaultURLSchemeHandler(String urlScheme) {
        try {
            return IsApplicationTheDefaultURLSchemeHandler(urlScheme, LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER);
        } catch(UnsatisfiedLinkError ule) {
            LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule); 
        }
        
        return true;
    }
    
    /**
     * This checks whether any applications are registered as handlers for this fileType in the OS-X
     * launch services database.
     * 
     * @param fileType -- the file extension for the file. this will be used to look up the file type's UTI (universal type identifier)
     * @return true if any application is registered as a handler for this file type
     */
    public static boolean isFileTypeHandled(String fileType) {
        try {
            return IsFileTypeHandled(fileType);
        } catch(UnsatisfiedLinkError ule) {
            LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule); 
        }
        
        return true;
    }

    /**
     * This checks whether any applications are registered as handlers for this URL scheme in the OS-X
     * launch services database.
     * 
     * @param urlScheme -- the designator for the protocol, e.g. magnet
     * @return true if any application is registered as a handler for this URL scheme
     */
    public static boolean isURLSchemeHandled(String urlScheme) {
        try {
            return IsURLSchemeHandled(urlScheme);
        } catch(UnsatisfiedLinkError ule) {
            LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule); 
        }
        
        return true;
    }

    /**
     * This tries to change the file type handler for the given file type from LimeWire to another application.
     * Basically, it just changes the default handler application to the first application in the list
     * returned by launch services that isn't LimeWire. It might fail if no other handlers are registered for this file type.  
     * The list of handlers that are used internally in this method should not be shown to users as they are probably not understandable
     * by users.  For example LimeWire is represented by the application bundle identifier com.limegroup.gnutella.
     * 
     * @param fileType -- the file extension for the file. this will be used to look up the file type's UTI (universal type identifier)
     */
    public static void tryChangingDefaultFileTypeHandler(String fileType) {
        try {
            String[] handlers = GetAllHandlersForFileType(fileType);
            if (handlers != null) {
                for (String handler : handlers) {
                    if (!handler.equals(LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER)) {
                        SetDefaultFileTypeHandler(fileType, handler);
                        break;
                    }
                }
            }
        } catch(UnsatisfiedLinkError ule) {
            LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule); 
        }
    }

    /**
     * This method returns true if there are any other applications on the user's system that have
     * registered themselves as handlers for the given file type.
     * 
     * @param fileType -- the file extension for the file. this will be used to look up the file type's UTI (universal type identifier)
     */

    public static boolean canChangeDefaultFileTypeHandler(String fileType) {
        try {
            String[] handlers = GetAllHandlersForFileType(fileType);
            if (handlers != null) {
                for (String handler : handlers) {
                    if (!handler.equals(LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER)) {
                        return true;
                    }
                }
            }
        } catch(UnsatisfiedLinkError ule) {
            LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule); 
        }
        
        return false;
    }
    
    /**
     * This tries to change the URL scheme handler for the given file type from LimeWire to another application.
     * Basically, it just changes the default handler application to the first application in the list
     * returned by launch services that isn't LimeWire. It might fail if no other handlers are registered for this URL scheme.  
     * The list of handlers that are used internally in this method should not be shown to users as they are probably not understandable
     * by users.  For example LimeWire is represented by the application bundle identifier com.limegroup.gnutella.
     * 
     * @param urlScheme -- the designator for the protocol, e.g. magnet
     */
    public static void tryChangingDefaultURLSchemeHandler(String urlScheme) {
        try {
            String[] handlers = GetAllHandlersForURLScheme(urlScheme);
            if (handlers != null) {
                for (String handler : handlers) {
                    if (!handler.equals(LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER)) {
                        SetDefaultURLSchemeHandler(urlScheme, handler);
                        break;
                    }
                }
            }
        } catch(UnsatisfiedLinkError ule) {
            LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule); 
        }
    }

    /**
     * This method returns true if there are any other applications on the user's system that have
     * registered themselves as handlers for the given URL scheme.
     * 
     * @param urlScheme -- the designator for the protocol, e.g. magnet
     */
    public static boolean canChangeDefaultURLSchemeHandler(String urlScheme) {
        try {
            String[] handlers = GetAllHandlersForURLScheme(urlScheme);
            if (handlers != null) {
                for (String handler : handlers) {
                    if (!handler.equals(LIMEWIRE_APPLICATION_BUNDLE_IDENTIFIER)) {
                        return true;
                    }
                }
            }
        } catch(UnsatisfiedLinkError ule) {
            LOG.error("UnsatisfiedLinkError for MacOSXUtils", ule); 
        }
        
        return false;
    }
    
    /**
     * This method opens up a native OS-X file dialog. These native dialogs are better than the FileDialog and JFileChooser
     * currently available in jdk6, because they have the native look and feel and navigation features of a FileDialog, but
     * they also allow for multiple file selections as JFileChoosers do.  If a native file dialog cannot be opened
     * however because the native library cannot be found, then this shows a Java based dialog instead.
     *
     * @param dialogTitle - the title to be shown in the dialog. this should already have been translated.
     * @param directory - the directory that the file dialog should open to
     * @param canChooseFiles - whether files can be selected
     * @param canChooseDirectories - whether directories can be selected
     * @param allowMultipleSelections - whether multiple files or directories can be selected
     * @return an array of file objects that were selected by the user or null if the user canceled the operation
     */
    public static List<File> openNativeFileDialog(final String dialogTitle, final File directory, final boolean canChooseFiles, 
                                                  final boolean canChooseDirectories, final boolean allowMultipleSelections,
                                                  final FileFilter filter) {
        try {
            final String directoryAbsolutePath = (directory != null) ? directory.getAbsolutePath() : null;

            String[] filePaths = SwingUtils.runOffEventDispatchThread(
                    new java.util.concurrent.Callable<String[]>() {
                        @Override
                        public String[] call() {
                            return OpenNativeFileDialog(dialogTitle, directoryAbsolutePath,
                                    canChooseFiles, canChooseDirectories, allowMultipleSelections);
                        }
                    },
                    "mac-native-file-dialog");
            
            if (filePaths == null) {
                return null;
            } else {
                List<File> selectedFileList = new ArrayList<File>();
                for (String filePath : filePaths) {
                    File selectedFile = new File(filePath);
                    // since we couldn't pass the file filter over to the native dialog, 
                    // let's filter the files here if the filter is not null...
                    if (filter != null) {
                        if (filter.accept(selectedFile)) {
                            selectedFileList.add(selectedFile);
                        }
                    } else {
                        selectedFileList.add(selectedFile);                       
                    }
                }
                             
                return selectedFileList;
            }
        } catch(UnsatisfiedLinkError ule) {
            // If we can't open up a native file dialog, then let's open a Java dialog in the same way we did before we
            // started using native dialogs
            return openJavaFileDialog(dialogTitle, directory, canChooseFiles, canChooseDirectories, allowMultipleSelections, filter);
        }
    }

    /**
     * This opens up a Java file dialog that's been fine tuned for OS-X for selecting files or directories.
     * Java based file dialogs (as of JDK6) do not allow users to select multiple files or directories.
     * Clients should prefer to use the openNativeFileDialog() method rather than this one,
     * and for this reason this method is currently private.
     * This method is intended to serve only as a fallback if the native library for opening
     * native file dialogs cannot be loaded.
     * 
     * @param dialogTitle - the title to be shown in the dialog. this should already have been translated.
     * @param directory - the directory that the file dialog should open to
     * @param canChooseFiles - whether files can be selected
     * @param canChooseDirectories - whether directories can be selected
     * @param allowMultipleSelections - whether multiple files or directories can be selected. (This is ignored 
     *                                  when using a Java based dialog.)
     * @param filter - a file filter for disallowing users to select certain files
     * @return an array of file objects that were selected by the user or null if the user canceled the operation
     */
    private static List<File> openJavaFileDialog(String dialogTitle, File directory, boolean canChooseFiles, 
                                                boolean canChooseDirectories, boolean allowMultipleSelections,
                                                final FileFilter filter) {        
        FileDialog dialog;
        if(canChooseDirectories && !canChooseFiles) {
            dialog = MacUtils.getFolderDialog(null);
        } else {
            dialog = new FileDialog(new LimeJFrame(), "");
        }
        
        dialog.setTitle(dialogTitle);
        
        if(filter != null) {
            FilenameFilter f = new FilenameFilter() {
                @Override
				public boolean accept(File dir, String name) {
                    return filter.accept(new File(dir, name));
                }
            };
            dialog.setFilenameFilter(f);
        }
        
        dialog.setVisible(true);
        String dirStr = dialog.getDirectory();
        String fileStr = dialog.getFile();
        if((dirStr==null) || (fileStr==null))
            return null;

        // if the filter didn't work, pretend that the person picked
        // nothing
        File f = new File(dirStr, fileStr);
        if(filter != null && !filter.accept(f))
            return null;
        
        return Collections.singletonList(f);        
    }
    
    /**
     * Uses OS-X's launch services API to check whether any application has registered itself
     * as a handler for this file type. 
     */
    private static final native boolean IsFileTypeHandled(String fileType);

    /**
     * Uses OS-X's launch services API to check whether any application has registered itself
     * as a handler for this URL scheme. 
     */
    private static final native boolean IsURLSchemeHandled(String urlScheme);

    /**
     * Uses OS-X's launch services API to check whether the given application is the default handler for this
     * file type.
     */
    private static final native boolean IsApplicationTheDefaultFileTypeHandler(String fileType, String applicationBundleIdentifier);

    /**
     * Uses OS-X's launch services API to check whether the given application is the default handler for this
     * URL scheme.
     */
    private static final native boolean IsApplicationTheDefaultURLSchemeHandler(String urlScheme, String applicationBundleIdentifier);

    /**
     * Uses OS-X's launch services API to set the given application as the default handler for this file type.
     */
    private static final native int SetDefaultFileTypeHandler(String fileType, String applicationBundleIdentifier);

    /**
     * Uses OS-X's launch services API to set the given application as the default handler for this URL scheme.
     */
    private static final native int SetDefaultURLSchemeHandler(String urlScheme, String applicationBundleIdentifier);
    
    /**
     * Uses OS-X's launch services API to get all the handlers for this file type.
     */
    private static final native String[] GetAllHandlersForFileType(String fileType); 

    /**
     * Uses OS-X's launch services API to get all the handlers for this URL scheme.
     */
    private static final native String[] GetAllHandlersForURLScheme(String ulrScheme); 
    
    /**
     * Open a native file dialog for selecting files and folders.
     * Native dialogs have the advantage of preserving the look and feel of
     * the platform while still allowing for multiple file selections.
     */
    private static final native String[] OpenNativeFileDialog(String title, String directoryPath, boolean canChooseFiles, 
                                                              boolean canChooseDirectories, boolean allowMultipleSelections);

    /**
     * Gets the full user's name.
     */
    private static final native String GetCurrentFullUserName();
    
    /**
     * [Un]registers LimeWire from the startup items list.
     */
    private static final native void SetLoginStatusNative(boolean allow);
}
