/* GraphURI.java
 * =========================================================================
 * This file is part of the GrInvIn project - http://www.grinvin.org
 * 
 * Copyright (C) 2005-2008 Universiteit Gent
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 * 
 * A copy of the GNU General Public License can be found in the file
 * LICENSE.txt provided with the source distribution of this program (see
 * the META-INF directory in the source jar). This license can also be
 * found on the GNU website at http://www.gnu.org/licenses/gpl.html.
 * 
 * If you did not receive a copy of the GNU General Public License along
 * with this program, contact the lead developer, or write to the Free
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

package org.grinvin.graphs;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.grinvin.factories.FactoryException;
import org.grinvin.factories.FactoryManager;
import org.grinvin.factories.FactoryParameterException;
import org.grinvin.factories.graphs.GraphFactory;
import org.grinvin.io.graphs.GraphBundleLoader;
import org.grinvin.io.IOFormatException;
import org.grinvin.io.InvariantValuesLoader;
import org.grinvin.io.SectionLoader;
import org.grinvin.params.ParameterList;
import org.grinvin.preferences.GrinvinPreferences;
import org.grinvin.preferences.GrinvinPreferences.Preference;

/**
 * Utility class provides a method {@link #load} to load a graph that corresponds
 * to an URI.
 * Currently the following URIs are supported:
 * <ul>
 * <li>Any absolute URI which can be converted to an URL with a supported protocol handler.
 * I.e., URIs with schemes like <tt>file:</tt>, <tt>http:</tt>, <tt>jar:</tt>.
 * The resources are supposed to refer to streams in <tt>.gph</tt>-format.
 * See package <a href="io/package-summary.html#package_description">org.grinvin.io</a>
 * for more information on this format.</li>
 * <li>An absolute URI with scheme <tt>graph:</tt> which represents a graph that
 * can be created by a {@link GraphFactory}.</li>
 * <li>An absolute URI with scheme <tt>classpath:</tt> which represents a graph
 * resource on the class path.</li>
 * <li>A relative URI with scheme <tt>session:</tt> which represents a graph
 * in the current workspace directory.</li>
 * <li>A null URI is equivalent to an URI with scheme <tt>session:</tt>. A graph
 * with a null URI will be assigned a session URI when needed.</li>
 * </ul>
 * An <tt>graph:</tt> URI has the following form:
 * <blockquote>
 *   <b>graph:</b><i>graph_factory-id</i>[<b>?</b><i>name-value-pairs</i>]
 * </blockquote>
 * <p>The <i>graph factory id</i> identifies an object of type {@link GraphFactory}. 
 * If present, the <i>name/value pairs</i> indicate
 * additional parameters for that factory. They are formatted in a style similar
 * to that of HTTP query strings:
 * <blockquote>
 *    name1<b>=</b>value1<b>&</b>name2<b>=</b>value2<b>&</b>...<b>&</b>name3<b>=</b>value3</b>
 * </blockquote>
 * E.g., the following represents the complete graph of order 4
 * <pre>
 *     graph:org.grinvin.factories.CompleteGraphFactory?order=4
 * </pre>
 */
public final class GraphURI {
    
    // make sure clients do not instantiate this class
    private GraphURI() {}
    
    //
    public static boolean isFactoryGenerated(URI uri) {
        return compareScheme("graph", uri);
    }
    
    //
    public static boolean isClasspath(URI uri) {
        return compareScheme("classpath", uri);
    }
    
    //
    public static boolean isFile(URI uri) {
        return compareScheme("file", uri);
    }
    
    //
    public static boolean isGlobal(URI uri) {
        return isFactoryGenerated(uri) || isClasspath(uri);
    }
    
    //
    public static boolean isLocal(URI uri) {
        return isFile(uri);
    }
    
    /**
     * Is this a null URI or an URI with a 'session' scheme. URIs of this
     * type correspond to graphs that reside in memory only (and can be
     * persisted to a workspace).
     */
    public static boolean isSession(URI uri) {
        if (uri == null)
            return true;
        else
            return "session".equals(uri.getScheme());
    }
    
    //
    public static URI createFactory(String ssp) throws URISyntaxException {
        return new URI("graph", ssp, null);
    }
    
    //
    public static URI createSession(String ssp) throws URISyntaxException {
        return new URI("session", ssp, null);
    }
    
    //
    private static boolean compareScheme(String scheme, URI uri) {
        return (uri != null && uri.getScheme().equals(scheme)) ;
    }
    
    /**
     * Return the type corresponding to the given URI.
     */
    public static GraphURIType getType(URI uri) {
        if (GraphURI.isSession(uri)) {
            return GraphURIType.GRAPH_SESSION;
        } else if (GraphURI.isGlobal(uri)) {
            return GraphURIType.GRAPH_GLOBAL;
        } else { // GraphURI.isLocal(uri)
            return GraphURIType.GRAPH_LOCAL;
        }
    }
    
    private static class ClasspathSectionLoader implements SectionLoader {
        public InputStream openSection(String name) throws IOException {
            return GraphURI.class.getResource(name).openStream();
        }
    }
    
    /**
     * Load the graph that corresponds to the given URI into the given graph bundle.
     * @param uri URI that represents the graph
     * @param graphBundle Graph bundle which will hold the result. Preferably empty.
     * @throws GraphURIException when the graph could not be obtained from the given URI.
     */
    public static void load(URI uri, GraphBundle graphBundle, SectionLoader sloader) throws GraphURIException {
        
        if (! uri.isAbsolute()) {
            throw new GraphURIException("URI should be absolute: " + uri);
        }
        try {
            if (isClasspath(uri)) {
                URL url = GraphURI.class.getResource(uri.getSchemeSpecificPart());
                if (url == null)
                    throw new GraphURIException("Graph could not be found: " + uri);
                URL metaInfoURL = GraphURI.class.getResource(uri.getSchemeSpecificPart() + "/meta-info.xml");
                if (metaInfoURL == null)
                    GraphBundleLoader.loadFromZip(graphBundle,url.openStream());
                else
                    GraphBundleLoader.load(graphBundle, new ClasspathSectionLoader(), uri.getSchemeSpecificPart());
            } else if (isGlobal(uri)) {
                String raw = uri.getRawSchemeSpecificPart();
                int pos = raw.indexOf('?');
                if (pos >= 0)
                    loadGraphScheme(raw.substring(0,pos), raw.substring(pos+1), graphBundle);
                else
                    loadGraphScheme(raw, "", graphBundle);
            } else if (isSession(uri)) {
                if (sloader != null)
                    GraphBundleLoader.load(graphBundle, sloader, uri.getSchemeSpecificPart());
                else
                    throw new GraphURIException("Session graph could not be loaded without SectionLoader: " + uri);
            } else { // file: uri (on local file system)
                try {
                    URL url = uri.toURL();
                    File directory = new File(uri);
                    if (directory.isDirectory()) {
                        GraphBundleLoader.loadFromDirectory(graphBundle, directory);
                    } else {
                        GraphBundleLoader.loadFromZip(graphBundle,url.openStream());
                    }
                } catch (MalformedURLException ex) {
                    throw new IllegalArgumentException("URI not supported: " + uri);
                }
            }
        } catch (IOException ioex) {
            throw new GraphURIException("URI could not be loaded", ioex);
        }
        
    }
    
    /**
     * Load a graph factory with the given id.
     */
    private static GraphFactory getFactory(String rawFactoryId) throws GraphURIException {
        try {
            String factoryId = URLDecoder.decode(rawFactoryId, "UTF-8");
            return FactoryManager.getGraphFactoryBroker().get(factoryId);
        } catch (UnsupportedEncodingException ex) {
            throw new RuntimeException("Encoding UTF-8 unexpectedly not known", ex);
        } 
    }
    
    /**
     * Load a graph for a 'graph:'-URI.
     */
    private static void loadGraphScheme(String rawFactoryId, String queryString, GraphBundle bundle) throws GraphURIException {
        GraphFactory factory = getFactory(rawFactoryId);
        if (factory == null)
            throw new GraphURIException("Could not create factory " + rawFactoryId);
        ParameterList list = factory.getParameters();
        try {
            factory.setParameterValues(list.parseQueryString(queryString));
            factory.createGraph(bundle);
            
            //load cached invariant values
            String cachedFilename = rawFactoryId + "-" + queryString.replace('&', '_').replace('=','_') + ".xml";
            File cachedFile = new File(GrinvinPreferences.getInstance().getStringPreference(Preference.GRINVIN_CACHE_DIR) + "/invariantvalues/" + cachedFilename);
            if (cachedFile.exists()) {
                try {
                    InvariantValuesLoader.load(bundle, new FileInputStream(cachedFile));
                } catch (IOFormatException ex) {
                    //something wrong with the xml file
                    //ignore this, the file will be regenerated on the next save
                    Logger.getLogger("org.grinvin.io").log(Level.WARNING, "Failed to load cached invariant values for " + rawFactoryId + "?" + queryString, ex);
                }
            } else {
                // import old values
                cachedFile = new File(GrinvinPreferences.getInstance().getStringPreference(Preference.GRINVIN_CACHE_DIR_1_0) + "/" + cachedFilename);
                if (cachedFile.exists()) {
                    try {
                        InvariantValuesLoader.load_1_0(bundle, new FileInputStream(cachedFile));
                    } catch (IOFormatException ex) {
                        //something wrong with the xml file
                        //ignore this, the file will be regenerated on the next save
                        Logger.getLogger("org.grinvin.io").log(Level.WARNING, "Failed to import old cached invariant values for " + rawFactoryId + "?" + queryString, ex);
                    }
                }
            }
            
        } catch (FactoryParameterException ex) {
            throw new GraphURIException("Could not initialize parameters", ex);
        } catch (FactoryException ex) {
            throw new GraphURIException("Could not create graph", ex);
        } catch (IllegalArgumentException ex) {
            throw new GraphURIException("Could not handle parameters", ex);
        } catch (Exception ex) {
            // makes sure Grinvin does not go down with defunct graph factories
            throw new GraphURIException("Undefined exception in factory", ex);
        }
    }
}
