package prefuse.util.io;

import java.awt.Component;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;

import javax.swing.JFileChooser;

import prefuse.data.Graph;
import prefuse.data.Table;
import prefuse.data.io.CSVTableReader;
import prefuse.data.io.DelimitedTextTableReader;
import prefuse.data.io.GraphMLReader;
import prefuse.data.io.GraphReader;
import prefuse.data.io.TableReader;
import prefuse.data.io.TreeMLReader;
import prefuse.util.StringLib;
import prefuse.util.collections.ByteArrayList;

/**
 * Library routines for input/output tasks.
 *  
 * @author <a href="http://jheer.org">jeffrey heer</a>
 */
public class IOLib {

    private IOLib() {
        // disallow instantiation
    }
    
    /**
     * Indicates if a given String is a URL string. Checks to see if the string
     * begins with the "http:/", "ftp:/", or "file:/" protocol strings.
     * @param s the string to check
     * @return true if a url string matching the listed protocols,
     * false otherwise
     */
    public static boolean isUrlString(String s) {
        return s.startsWith("http:/") ||
               s.startsWith("ftp:/")  ||
               s.startsWith("file:/");
    }
    
    /**
     * From a string description, attempt to generate a URL object. The string
     * may point to an Internet location (e.g., http:// or ftp:// URL),
     * a resource on the class path (resulting in a resource URL that points
     * into the current classpath), or a file on the local filesystem
     * (resulting in a file:// URL). The String will be checked in that order
     * in an attempt to resolve it to a valid URL.
     * @param location the location string for which to get a URL object
     * @return a URL object, or null if the location string could not be
     * resolved
     */
    public static URL urlFromString(String location) {
        return urlFromString(location, null, true);
    }
    
    /**
     * From a string description, attempt to generate a URL object. The string
     * may point to an Internet location (e.g., http:// or ftp:// URL),
     * a resource on the class path (resulting in a resource URL that points
     * into the current classpath), or, if the <code>includeFileSystem</code>
     * flag is true, a file on the local filesystem
     * (resulting in a file:// URL). The String will be checked in that order
     * in an attempt to resolve it to a valid URL.
     * @param location the location string for which to get a URL object
     * @param referrer the class to check for classpath resource items, the
     * location string will be resolved against the package/folder containing
     * this class 
     * @param includeFileSystem indicates if the file system should be
     * included in the search to resolve the location String
     * @return a URL object, or null if the location string could not be
     * resolved
     */
    public static URL urlFromString(String location, Class referrer,
                                    boolean includeFileSystem)
    {
        URL url = null;
        if ( isUrlString(location) ) {
            // explicit URL string
            try {
                url = new URL(location);
            } catch ( Exception e ) {
                e.printStackTrace();
            }
        } else {
            // attempt to get a URL pointing into the classpath
            if ( referrer != null )
                url = referrer.getResource(location);
            else
                url = IOLib.class.getResource(location);

            if ( url == null && !location.startsWith("/") )
                url = IOLib.class.getResource("/"+location);
            
            if ( includeFileSystem && url == null ) {
                // if still not found, check the file system
                if ( (new File(location)).exists() ) {
                    try {
                        url = new URL("file:///"+location);
                    } catch ( Exception e ) {}
                }
            }
        }
        return url;
    }
    
    /**
     * Get an input string corresponding to the given location string. The
     * string will first be resolved to a URL and an input stream will be
     * requested from the URL connection. If this fails, the location will
     * be resolved against the file system. Also, if a gzip file is found,
     * the input stream will also be wrapped by a GZipInputStream. If the
     * location string can not be resolved, a null value is returned
     * @param location the location string
     * @return an InputStream for the resolved location string
     * @throws IOException if an input/ouput error occurs
     */
    public static InputStream streamFromString(String location) 
        throws IOException
    {
        InputStream is = null;
        
        // try to get a working url from the string
        URL url = urlFromString(location, null, false);
        if ( url != null ) {
            is = url.openStream();
        } else {
            // if that failed, try the file system
            File f = new File(location);
            if ( f.exists() )
                is = new FileInputStream(f);
        }
        
        if ( is == null ) {
            return null; // couldn't find it
        } else if ( isGZipFile(location) ) {
            return new GZIPInputStream(is);
        } else {
            return is;
        }
    }
    
    /**
     * Returns the extension for a file or null if there is none
     * @param f the input file
     * @return the file extension, or null if none
     */
    public static String getExtension(File f) {
        return (f != null ? getExtension(f.getName()) : null);
    }
    
    /**
     * Indicates if the given file ends with a file extension of
     * ".gz" or ".Z", indicating a GZip file.
     * @param file a String of the filename or URL of the file
     * @return true if the extension is ".gz" or ".Z", false otherwise
     */
    public static boolean isGZipFile(String file) {
    	String ext = getExtension(file);
        return "gz".equals(ext) || "z".equals(ext);
    }
    
    /**
     * Indicates if the given file ends with a file extension of
     * ".zip", indicating a Zip file.
     * @param file a String of the filename or URL of the file
     * @return true if the extension is ".zip", false otherwise
     */
    public static boolean isZipFile(String file) {
        return "zip".equals(getExtension(file));
    }
    
    /**
     * Returns the extension for a file or null if there is none
     * @param filename the input filename
     * @return the file extension, or null if none
     */
    public static String getExtension(String filename) {
        int i = filename.lastIndexOf('.');
        if ( i>0 && i<filename.length()-1 ) {
            return filename.substring(i+1).toLowerCase();
        } else {
            return null;
        }
    }
    
    /**
     * Reads an input stream into a list of byte values.
     * @param is the input stream to read
     * @return a ByteArrayList containing the contents of the input stream
     * @throws IOException if an input/ouput error occurs
     */
    public static ByteArrayList readAsBytes(InputStream is) throws IOException {
        ByteArrayList buf = new ByteArrayList();
        byte[] b = new byte[8192];
        int nread = -1;
        while ( (nread=is.read(b)) >= 0 ) {
            buf.add(b, 0, nread);
        }
        return buf;
    }
    
    /**
     * Reads an input stream into a single String result.
     * @param is the input stream to read
     * @return a String containing the contents of the input stream
     * @throws IOException if an input/ouput error occurs
     */
    public static String readAsString(InputStream is) throws IOException {
        StringBuffer buf = new StringBuffer();
        byte[] b = new byte[8192];
        int nread = -1;
        while ( (nread=is.read(b)) >= 0 ) {
            String s = new String(b, 0, nread);
            buf.append(s);
        }
        return buf.toString();
    }
    
    /**
     * Reads data pulled from the given location string into a single String
     * result. The method attempts to retrieve an InputStream using the
     * {@link #streamFromString(String)} method, then read the input stream
     * into a String result.
     * @param location the location String
     * @return a String with the requested data
     * @throws IOException if an input/ouput error occurs
     * @see #streamFromString(String)
     */
    public static String readAsString(String location) throws IOException {
        return readAsString(streamFromString(location));
    }
    
    // ------------------------------------------------------------------------
    
    /**
     * Present a file chooser dialog for loading a Table data set.
     * @param c user interface component from which the request is being made
     * @return a newly loaded Table, or null if not found or action canceled
     */
    public static Table getTableFile(Component c) {
        JFileChooser jfc = new JFileChooser();
        jfc.setDialogType(JFileChooser.OPEN_DIALOG);
        jfc.setDialogTitle("Open Table File");
        jfc.setAcceptAllFileFilterUsed(false);
        
        SimpleFileFilter ff;
        
        // TODO: have this generate automatically
        // tie into PrefuseConfig??
        
        // CSV
        ff = new SimpleFileFilter("csv",
                "Comma Separated Values (CSV) File (*.csv)",
                new CSVTableReader());
        ff.addExtension("gz");
        jfc.setFileFilter(ff);

        // Pipe-Delimited
        ff = new SimpleFileFilter("txt",
                "Pipe-Delimited Text File (*.txt)",
                new DelimitedTextTableReader("|"));
        ff.addExtension("gz");
        jfc.setFileFilter(ff);

        // Tab-Delimited
        ff = new SimpleFileFilter("txt",
                "Tab-Delimited Text File (*.txt)",
                new DelimitedTextTableReader());
        ff.addExtension("gz");
        jfc.setFileFilter(ff);
        
        int retval = jfc.showOpenDialog(c);
        if (retval != JFileChooser.APPROVE_OPTION)
            return null;
        
        File f = jfc.getSelectedFile();
        ff = (SimpleFileFilter)jfc.getFileFilter();
        TableReader tr = (TableReader)ff.getUserData();
        
        try {
            return tr.readTable(streamFromString(f.getAbsolutePath()));
        } catch ( Exception e ) {
            Logger.getLogger(IOLib.class.getName()).warning(
                e.getMessage() + "\n" + StringLib.getStackTrace(e));
            return null;
        }
    }
    
    /**
     * Present a file chooser dialog for loading a Graph or Tree data set.
     * @param c user interface component from which the request is being made
     * @return a newly loaded Graph, or null if not found or action canceled
     */
    public static Graph getGraphFile(Component c) {
        JFileChooser jfc = new JFileChooser();
        jfc.setDialogType(JFileChooser.OPEN_DIALOG);
        jfc.setDialogTitle("Open Graph or Tree File");
        jfc.setAcceptAllFileFilterUsed(false);
        
        SimpleFileFilter ff;
        
        // TODO: have this generate automatically
        // tie into PrefuseConfig??

        // TreeML
        ff = new SimpleFileFilter("xml",
                "TreeML File (*.xml, *.treeml)",
                new TreeMLReader());
        ff.addExtension("treeml");
        ff.addExtension("gz");
        jfc.setFileFilter(ff);
        
        // GraphML
        ff = new SimpleFileFilter("xml",
                "GraphML File (*.xml, *.graphml)",
                new GraphMLReader());
        ff.addExtension("graphml");
        ff.addExtension("gz");
        jfc.setFileFilter(ff);
        
        int retval = jfc.showOpenDialog(c);
        if (retval != JFileChooser.APPROVE_OPTION)
            return null;
        
        File f = jfc.getSelectedFile();
        ff = (SimpleFileFilter)jfc.getFileFilter();
        GraphReader gr = (GraphReader)ff.getUserData();
        
        try {
            return gr.readGraph(streamFromString(f.getAbsolutePath()));
        } catch ( Exception e ) {
            Logger.getLogger(IOLib.class.getName()).warning(
                e.getMessage() + "\n" + StringLib.getStackTrace(e));
            return null;
        }
    }
    
} // end of class IOLib
