/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.riot.adapters;

import java.io.File;
import java.io.InputStream;
import java.util.Iterator ;

import org.apache.jena.atlas.io.IO;
import org.apache.jena.atlas.logging.FmtLog;
import org.apache.jena.atlas.web.TypedInputStream ;
import org.apache.jena.irix.IRIs;
import org.apache.jena.rdf.model.Model ;
import org.apache.jena.rdf.model.ModelFactory;
import org.apache.jena.riot.Lang ;
import org.apache.jena.riot.RDFDataMgr ;
import org.apache.jena.riot.RDFLanguages ;
import org.apache.jena.riot.system.stream.* ;
import org.apache.jena.shared.JenaException;
import org.apache.jena.shared.NotFoundException;
import org.apache.jena.util.FileManager ;
import org.apache.jena.util.FileUtils ;
import org.apache.jena.util.TypedStream ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;

/** FileManager
 *
 * A FileManager provides access to named file-like resources by opening
 * InputStreams to things in the filing system, by URL (http: and file:) and
 * found by the classloader.  It can also load RDF data from such a system
 * resource into an existing model or create a new (Memory-based) model.
 * There is a global FileManager which provide uniform access to system
 * resources: applications may also create specialised FileManagers.
 *
 * A FileManager contains a list of location functions to try: the global
 * FileManger has one {@link LocatorFile}, one {@link LocatorClassLoader} and
 * one {@link LocatorURL}
 *
 * Main operations:
 * <ul>
 * <li>loadModel, readModel : URI to model</li>
 * <li>open, openNoMap : URI to input stream</li>
 * <li>mapURI : map URI to another by {@link LocationMapper}</li>
 * </ul>
 *
 * Utilities:
 * <ul>
 * <li>readWholeFileAsUTF8</li>
 * </ul>
 *
 * A FileManager works in conjunction with a LocationMapper.
 * A {@link LocationMapper} is a set of alternative locations for system
 * resources and a set of alternative prefix locations.  For example, a local
 * copy of a common RDF dataset may be used whenever the usual URL is used by
 * the application.
 *
 * The {@link LocatorFile} also supports the idea of "current directory".
 *
 * @see StreamManager
 * @see LocationMapper
 * @see FileUtils
 */

public class AdapterFileManager implements org.apache.jena.util.FileManager
{
    // This is a legacy class - it provides  FileManager calls onto the RIOT equivalents.
    // The different aspects are now split out
    // and this class maintains the old interface.
    // Aspects: StreamManager, LocationMapper, Model cache.
    // and this class exists to maintain the old interfaces and the
    // combination of stream and model reading.

    // RIOT reader uses StreamManager.
    // Each FileManager has a StreamManager.

    /** Delimiter between path entries : because URI scheme names use : we only allow ; */
    //public static final String PATH_DELIMITER = ";";
    //public static final String filePathSeparator = java.io.File.separator ;

    private static Logger log = LoggerFactory.getLogger(AdapterFileManager.class) ;

    private static AdapterFileManager instance = null ;
    private final StreamManager streamManager ;

    // -------- Cache operations
    // See also loadModelWorker which uses the cache.
    // These are in the FileManager for legacy reasons.
    private FileManagerModelCache modelCache = new FileManagerModelCache() ;

    /**
     * Get the global file manager.
     *
     * @return the global file manager
     */
    public static AdapterFileManager get() {
        if ( instance == null )
            instance = makeGlobal() ;
        return instance ;
    }

    /**
     * Set the global file manager (as returned by get()) If called before any
     * call to get(), then the usual default filemanager is not created
     *
     * @param globalFileManager
     */
    public static void setGlobalFileManager(AdapterFileManager globalFileManager) {
        instance = globalFileManager ;
    }

    /** Create an uninitialized StreamManager */
    private AdapterFileManager() {
        streamManager = new StreamManager() ;
    }

    @Override
    public FileManager clone() {
        StreamManager sm = streamManager.clone() ;
        AdapterFileManager x = new AdapterFileManager(sm) ;
        return x ;
    }

    public AdapterFileManager(StreamManager streamManager) {
        this(streamManager, streamManager == null ? null : streamManager.getLocationMapper()) ;
    }

    /** Create a FileManger using a RIOT StreamManager and RIOT LocationMapper */
    public AdapterFileManager(StreamManager streamManager, LocationMapper mapper) {
        if ( streamManager == null )
            streamManager = new StreamManager() ;
        this.streamManager = streamManager ;
        streamManager.setLocationMapper(mapper) ;
    }

    /** Create a "standard" FileManager. */
    public static AdapterFileManager makeGlobal() {
        AdapterFileManager fMgr = new AdapterFileManager(StreamManager.get()) ;
        return fMgr ;
    }

    /** Return the associate stream manager */
    public StreamManager getStreamManager() {
        return streamManager ;
    }

    /** Set the location mapping */
    @Override
    public void setLocationMapper(org.apache.jena.util.LocationMapper mapper) {
        streamManager.setLocationMapper(AdapterLib.copyConvert(mapper)) ;
    }

    /** Get the location mapping */
    @Override
    public org.apache.jena.util.LocationMapper getLocationMapper() {
        return new AdapterLocationMapper(streamManager.getLocationMapper()) ;
    }

    /** Return an iterator over all the handlers */
    @Override
    public Iterator<org.apache.jena.util.Locator> locators() {
        throw new UnsupportedOperationException() ;
    }

    /** Remove a locator */
    @Override
    public void remove(org.apache.jena.util.Locator loc) {
        throw new UnsupportedOperationException() ;
    }

    /** Add a locator to the end of the locators list */
    @Override
    public void addLocator(org.apache.jena.util.Locator oldloc) {
        Locator loc = AdapterLib.convert(oldloc) ;
        log.debug("Add location: " + loc.getName()) ;
        streamManager.addLocator(loc) ;
    }

    /** Add a file locator */
    @Override
    public void addLocatorFile() {
        addLocatorFile(null) ;
    }

    /** Add a file locator which uses dir as its working directory */
    @Override
    public void addLocatorFile(String dir) {
        LocatorFile fLoc = new LocatorFile(dir) ;
        streamManager.addLocator(fLoc) ;
    }

    /** Add a class loader locator */
    @Override
    public void addLocatorClassLoader(ClassLoader cLoad) {
        LocatorClassLoader cLoc = new LocatorClassLoader(cLoad) ;
        streamManager.addLocator(cLoc) ;
    }

    @Override
    public void addLocatorURL() {
        addLocatorHTTP();
    }

    public void addLocatorHTTP() {
        Locator loc = new LocatorHTTP() ;
        streamManager.addLocator(loc) ;
    }

    public void addLocatorFTP() {
        Locator loc = new LocatorFTP() ;
        streamManager.addLocator(loc) ;
    }


    /** Add a zip file locator */
    @Override
    public void addLocatorZip(String zfn) {
        Locator loc = new LocatorZip(zfn) ;
        streamManager.addLocator(loc) ;
    }

    // -------- Cache operations (start)
    /** Reset the model cache */
    @Override
    public void resetCache() {
        modelCache.resetCache() ;
    }

    /** Change the state of model cache : does not clear the cache */
    @Override
    public void setModelCaching(boolean state) {
        modelCache.setModelCaching(state) ;
    }

    /** return whether caching is on of off */
    @Override
    public boolean isCachingModels() {
        return modelCache.isCachingModels() ;
    }

    /** Read out of the cache - return null if not in the cache */
    @Override
    public Model getFromCache(String filenameOrURI) {
        return modelCache.getFromCache(filenameOrURI) ;
    }

    @Override
    public boolean hasCachedModel(String filenameOrURI) {
        return modelCache.hasCachedModel(filenameOrURI) ;
    }

    @Override
    public void addCacheModel(String uri, Model m) {
        modelCache.addCacheModel(uri, m) ;
    }

    @Override
    public void removeCacheModel(String uri) {
        modelCache.removeCacheModel(uri) ;
    }

    // -------- Cache operations (end)

    protected Model readModelWorker(Model model, String filenameOrURI, String baseURI, String syntax) {
        // Doesn't call open() - we want to make the syntax guess
        // based on the mapped URI.
        String mappedURI = mapURI(filenameOrURI) ;

        if ( log.isDebugEnabled() && !mappedURI.equals(filenameOrURI) )
            log.debug("Map: " + filenameOrURI + " => " + mappedURI) ;

        Lang lang =
            (syntax != null)
            ? RDFLanguages.nameToLang(syntax)
            : RDFLanguages.resourceNameToLang(mappedURI, Lang.RDFXML) ;

        // Not : RDFDataMgr.read(model, mappedURI, baseURI, lang);
        // Allow model.read to be overridden e.g. by OntModel which does import processing.
        if ( baseURI == null )
            baseURI = IRIs.toBase(filenameOrURI) ;
        try(TypedInputStream in = streamManager.openNoMapOrNull(mappedURI)) {
            if ( in == null )
            {
                FmtLog.debug(log, "Failed to locate '%s'", mappedURI);
                throw new NotFoundException("Not found: "+filenameOrURI) ;
            }
            Lang lang2 = RDFDataMgr.determineLang(mappedURI, in.getContentType(), lang);
            // May be overridden by model implementation.
            model.read(in, baseURI, lang2.getName()) ;
        }
        return model ;
    }

    /**
     * Open a file using the locators of this FileManager Throws
     * RiotNotFoundException if not found.
     */
    @Override
    public InputStream open(String filenameOrURI) {
        return streamManager.open(filenameOrURI) ;
    }

    /** Apply the mapping of a filename or URI */
    @Override
    public String mapURI(String filenameOrURI) {
        return streamManager.mapURI(filenameOrURI) ;
    }

    /**
     * Open a file using the locators of this FileManager but without location
     * mapping. Throws RiotNotFoundException if not found.
     */
    @Override
    public InputStream openNoMap(String filenameOrURI) {
        return streamManager.openNoMap(filenameOrURI) ;
    }

    /**
     * Open a file using the locators of this FileManager without location
     * mapping. Return null if not found
     */
    @Override
    public TypedStream openNoMapOrNull(String filenameOrURI) {
        return AdapterLib.convert(streamManager.openNoMapOrNull(filenameOrURI)) ;
    }

    // -------- Cache operations (end)

    // LEGACY

    @Override
    public Model loadModelInternal(String filenameOrURI)
    {
        if ( log.isDebugEnabled() )
            log.debug("loadModel("+filenameOrURI+")") ;

        return loadModelWorker(filenameOrURI, null, null) ;
    }

    /** Load a model from a file (local or remote).
     *  URI is the base for reading the model.
     *
     *  @param filenameOrURI The filename or a URI (file:, http:)
     *  @param rdfSyntax  RDF Serialization syntax.
     *  @return a new model
     *  @exception JenaException if there is syntax error in file.
     */
    @Deprecated
    @Override
    public Model loadModel(String filenameOrURI, String rdfSyntax)
    {
        if ( log.isDebugEnabled() )
            log.debug("loadModel("+filenameOrURI+", "+rdfSyntax+")") ;
        return loadModelWorker(filenameOrURI, null, rdfSyntax) ;
    }

    /** Load a model from a file (local or remote).
     *
     *  @param filenameOrURI The filename or a URI (file:, http:)
     *  @param baseURI  Base URI for loading the RDF model.
     *  @param rdfSyntax  RDF Serialization syntax.
     *  @return a new model
     *  @exception JenaException if there is syntax error in file.
    */
    @Deprecated
    @Override
    public Model loadModel(String filenameOrURI, String baseURI, String rdfSyntax)
    {
        if ( log.isDebugEnabled() )
            log.debug("loadModel("+filenameOrURI+", "+baseURI+", "+rdfSyntax+")") ;

        return loadModelWorker(filenameOrURI, baseURI, rdfSyntax) ;
    }

    private Model loadModelWorker(String filenameOrURI, String baseURI, String rdfSyntax)
    {
        if ( hasCachedModel(filenameOrURI) )
        {
            if ( log.isDebugEnabled() )
                log.debug("Model cache hit: "+filenameOrURI) ;
            return getFromCache(filenameOrURI) ;
        }

        Model m = ModelFactory.createDefaultModel() ;
        readModelWorker(m, filenameOrURI, baseURI, rdfSyntax) ;

        if ( isCachingModels() )
            addCacheModel(filenameOrURI, m) ;
        return m ;
    }

    @Override
    public Model readModelInternal(Model model, String filenameOrURI)
    {
        if ( log.isDebugEnabled() )
            log.debug("readModel(model,"+filenameOrURI+")") ;
        return readModel(model, filenameOrURI, null);
    }

    /**
     * Read a file of RDF into a model.
     * @param model
     * @param filenameOrURI
     * @param rdfSyntax RDF Serialization syntax.
     * @return The model or null, if there was an error.
     *  @exception JenaException if there is syntax error in file.
     */
    @Deprecated
    @Override
    public Model readModel(Model model, String filenameOrURI, String rdfSyntax)
    {
        if ( log.isDebugEnabled() )
            log.debug("readModel(model,"+filenameOrURI+", "+rdfSyntax+")") ;
        return readModelWorker(model, filenameOrURI, null, rdfSyntax);
    }

    /**
     * Read a file of RDF into a model.
     * @param model
     * @param filenameOrURI
     * @param baseURI
     * @param syntax
     * @return The model
     *  @exception JenaException if there is syntax error in file.
     */
    @Deprecated
    @Override
    public Model readModel(Model model, String filenameOrURI, String baseURI, String syntax)
    {

        if ( log.isDebugEnabled() )
            log.debug("readModel(model,"+filenameOrURI+", "+baseURI+", "+syntax+")") ;
        return readModelWorker(model, filenameOrURI, baseURI, syntax) ;
    }

    private static String chooseBaseURI(String baseURI)
    {
        String scheme = IRIs.scheme(baseURI) ;

        if ( scheme != null )
        {
            if ( scheme.equals("file") )
            {
                if ( ! baseURI.startsWith("file:///") )
                {
                    try {
                        // Fix up file URIs.  Yuk.
                        String tmp = baseURI.substring("file:".length()) ;
                        File f = new File(tmp) ;
                        baseURI = "file:///"+f.getCanonicalPath() ;
                        baseURI = baseURI.replace('\\','/') ;

//                        baseURI = baseURI.replace(" ","%20");
//                        baseURI = baseURI.replace("~","%7E");
                        // Convert to URI.  Except that it removes ///
                        // Could do that and fix up (again)
                        //java.net.URL u = new java.net.URL(baseURI) ;
                        //baseURI = u.toExternalForm() ;
                    } catch (Exception ex) {}
                }
            }
            return baseURI ;
        }

        if ( baseURI.startsWith("/") )
            return "file://"+baseURI ;
        return "file:"+baseURI ;
    }

    /**
     * @deprecated Use {@link IO#readWholeFileAsUTF8(InputStream)}
     */
    @Override
    @Deprecated
    public String readWholeFileAsUTF8(InputStream in) {
        return IO.readWholeFileAsUTF8(in);
    }

    /**
     * @deprecated Use {@link IO#readWholeFileAsUTF8(String)}
     */
    @Deprecated
    @Override
    public String readWholeFileAsUTF8(String filename)
    {
        InputStream in = open(filename) ;
        if ( in == null )
            throw new NotFoundException("File not found: "+filename) ;
        return readWholeFileAsUTF8(in) ;
    }
}
