/*
 * 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.system.stream ;

import java.util.ArrayList ;
import java.util.Collections ;
import java.util.List ;

import org.apache.jena.atlas.lib.Lib;
import org.apache.jena.atlas.web.TypedInputStream ;
import org.apache.jena.riot.RiotNotFoundException ;
import org.apache.jena.riot.SysRIOT;
import org.apache.jena.sparql.util.Context;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;

/**
 * Management of stream opening, including redirecting through a location mapper
 * whereby a name (e.g. URL) is redirected to another name (e.g. local file).
 * Includes filename to IRI, handling ".gz" and "-"
 */

public class StreamManager {
    // Need to combine with IO to do the .gz and "-" things.

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

    public static boolean        logAllLookups = true ;

    private List<Locator>        handlers      = new ArrayList<>() ;
    private LocationMapper       mapper        = null ;

    private static StreamManager globalStreamManager ;

    public StreamManager() {}

    /** Create a deep copy of this StreamManager */
    @Override
    public StreamManager clone() {
        return clone(this) ;
    }

    private static StreamManager clone(StreamManager other) {
        StreamManager sm = new StreamManager() ;
        sm.handlers.addAll(other.handlers) ;
        sm.mapper = other.mapper == null ? null : other.mapper.clone() ;
        return sm ;
    }

    /**
     * Return a default configuration StreamManager with a {@link LocatorFile},
     * {@link LocatorHTTP}, {@link LocatorFTP} and {@link LocatorClassLoader}
     */
    public static StreamManager makeDefaultStreamManager() {
        StreamManager streamManager = new StreamManager() ;
        streamManager.addLocator(new LocatorFile(null)) ;
        streamManager.addLocator(new LocatorHTTP()) ;
        streamManager.addLocator(new LocatorFTP()) ;
        streamManager.addLocator(new LocatorClassLoader(streamManager.getClass().getClassLoader())) ;
        streamManager.setLocationMapper(JenaIOEnvironment.getLocationMapper()) ;
        return streamManager ;
    }

    /**
     * Return the global {@code StreamManager}.
     */
    public static StreamManager get() {
        return globalStreamManager ;
    }

    /**
     * Return the {@code StreamManager} in a context, or the global one if the context is
     * null or does not contain a valid entry for a {@code StreamManager}.
     * <p>
     * The {@code StreamManager} is keyed in the context by
     * {@link SysRIOT#sysStreamManager}.
     */
    public static StreamManager get(Context context) {
        if ( context == null )
            return get();
        try {
            if ( context.isDefined(SysRIOT.sysStreamManager))
                return (StreamManager)context.get(SysRIOT.sysStreamManager);
        }
        catch (ClassCastException ex) {
            log.warn("Context symbol '" + SysRIOT.sysStreamManager + "' is not a " + Lib.classShortName(StreamManager.class));
        }
        return get();
    }

    /**
     * Set the global {@code StreamManager}.
     */
    public static void setGlobal(StreamManager streamManager) {
        globalStreamManager = streamManager ;
    }
    
    static { setGlobal(makeDefaultStreamManager()) ; }

    /**
     * Open a file using the locators of this StreamManager.
     * Returns null if not found.
     */
    public TypedInputStream open(String filenameOrURI) {
        if ( log.isDebugEnabled() )
            log.debug("open(" + filenameOrURI + ")") ;

        String uri = mapURI(filenameOrURI) ;

        if ( log.isDebugEnabled() && !uri.equals(filenameOrURI) )
            log.debug("open: mapped to " + uri) ;

        return openNoMapOrNull(uri) ;
    }

    /** Apply the mapping of a filename or URI */
    public String mapURI(String filenameOrURI) {
        if ( mapper == null )
            return filenameOrURI ;

        String uri = mapper.altMapping(filenameOrURI, null) ;

        if ( uri == null ) {
            if ( StreamManager.logAllLookups && log.isDebugEnabled() )
                log.debug("Not mapped: " + filenameOrURI) ;
            uri = filenameOrURI ;
        } else {
            if ( log.isDebugEnabled() )
                log.debug("Mapped: " + filenameOrURI + " => " + uri) ;
        }
        return uri ;
    }

    /**
     * Open a file using the locators of this FileManager but without location
     * mapping. Throws RiotNotFoundException if not found.
     */
    public TypedInputStream openNoMap(String filenameOrURI) {
        TypedInputStream in = openNoMapOrNull(filenameOrURI) ;
        if ( in == null )
            throw new RiotNotFoundException(filenameOrURI) ;
        return in ;
    }

    /**
     * Open a file using the locators of this FileManager without location
     * mapping. Return null if not found
     */

    public TypedInputStream openNoMapOrNull(String filenameOrURI) {
        for (Locator loc : handlers) {
            TypedInputStream in = loc.open(filenameOrURI) ;
            if ( in != null ) {
                if ( log.isDebugEnabled() )
                    log.debug("Found: " + filenameOrURI + " (" + loc.getName() + ")") ;
                return in ;
            }
        }
        return null ;
    }

    /** Set the location mapping */
    public void setLocationMapper(LocationMapper _mapper) {
        mapper = _mapper ;
    }

    /** Get the location mapping */
    public LocationMapper getLocationMapper() {
        return mapper ;
    }

    /** Return an immutable list of all the handlers */
    public List<Locator> locators() {
        return Collections.unmodifiableList(handlers) ;
    }

    /** Remove a locator */
    public void remove(Locator loc) {
        handlers.remove(loc) ;
    }

    /** Remove all locators */
    public void clearLocators() {
        handlers.clear() ;
    }

    /** Add a locator to the end of the locators list */
    public void addLocator(Locator loc) {
        handlers.add(loc) ;
    }
}
