/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.io.uri;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.Function;

import net.ranides.assira.collection.lists.RTList;
import net.ranides.assira.collection.maps.OpenMap;
import net.ranides.assira.collection.prototype.OpenGenericMap;
import net.ranides.assira.collection.prototype.GenericMap;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.credentials.UserProperty;
import net.ranides.assira.credentials.UserStore;
import net.ranides.assira.reflection.IClass;

/**
 * This is factory class which delegates URIHandle creation to one of registered URIHandler.
 *
 * You can associate UserStore with URIResolver.
 * Some protocols (for example HTTP and FTP) will try to use credentials from UserStore,
 * by invoking "find" method and putting resolved values for authentication.
 *
 * External user store allows you to avoid embedding user/password inside URIs.
 * Whats more, it allows you to use more sophisticated forms of authentication (SFTP, SSH would use keys)
 *
 * You can create create your own URIResolver and register handlers manually, or you can use
 * DEFAULT resolver which contains all handlers registered inside "META-INF/services".
 *
 * URIResolvers can be constructed from already configured factories.
 * They will copy registered handlers, but not associated UserStore.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public final class URIResolver {

    /**
     * Default URIResolver
     *
     * It uses all handlers discovered by scanning "META-INF/services".
     * Initially it uses empty UserStore, but it is allowed to associate additional stores with it.
     */
    public static final URIResolver DEFAULT = new URIResolver();

    static {
        for (URIHandler h : ServiceLoader.load(URIHandler.class)) {
            DEFAULT.register(h.getClass());
        }
    }

    private final MergeStore store = new MergeStore();

    private final List<Function<URIResolver, ? extends URIHandler>> registered;

    private final Map<String, URIHandler> schemes;

    /**
     * Creates new URIResolver, which does not contain any handlers.
     */
    public URIResolver() {
        registered = new RTList<>();
        schemes = new OpenMap<>();
    }

    /**
     * Creates new URIResolver with all handlers copied from source.
     * It does not copy UserStore configuration.
     *
     * @param source source
     */
    public URIResolver(URIResolver source) {
        registered = new RTList<>(source.registered);
        schemes = new OpenMap<>();
        for (Function<URIResolver, ? extends URIHandler> supplier : registered) {
            register0(supplier.apply(this));
        }
    }

    /**
     * Creates new URIHandle pointing to specified location.
     *
     * @param location location
     * @return URIHandle
     * @throws IOException
     *   if location has invalid format
     *   or location has unsupported scheme
     *   or URIHandler throws exception
     */
    public URIHandle resolve(String location) throws IOException {
        try {
            return resolve(new URI(location));
        } catch (URISyntaxException cause) {
            throw new IOException(cause);
        }
    }

    /**
     * Creates new URIHandle pointing to specified location.
     *
     * @param location location
     * @return URIHandle
     * @throws IOException
     *   if location has invalid format
     *   or location has unsupported scheme
     *   or URIHandler throws exception
     */
    public URIHandle resolve(File location) throws IOException{
        return resolve(location.toURI());
    }

    /**
     * Creates new URIHandle pointing to specified location.
     *
     * @param location location
     * @return URIHandle
     * @throws IOException
     *   if location has invalid format
     *   or location has unsupported scheme
     *   or URIHandler throws exception
     */
    public URIHandle resolve(URI location) throws IOException {
        String scheme = location.getScheme();
        URIHandler handler = schemes.get(scheme);
        if(null == handler) {
            throw new IOException("Unsupported scheme: " + scheme);
        }
        return handler.resolve(location);
    }

    /**
     * Registers URIHandler of provided type.
     * It constructs handler using default public constructor.
     *
     * @param handler handler
     * @return this
     */
    public URIResolver register(Class<? extends URIHandler> handler) {
        return register(IClass.typeinfo(handler)::construct);
    }

    /**
     * Registers URIHandler of provided type.
     * It constructs handler using provided generator function.
     * Function receives reference to "this" resolver as argument.
     *
     * @param handler handler
     * @return this
     */
    public URIResolver register(Function<URIResolver, ? extends URIHandler> handler) {
        URIHandler created = handler.apply(this);
        registered.add(handler);
        register0(created);
        return this;
    }

    private void register0(URIHandler handler) {
        for(String scheme : handler.schemes()) {
            schemes.put(scheme, handler);
        }
    }

    /**
     * Returns view with all supported schemes.
     * Schemes does not contain ":" characters.
     *
     * Please note: returned view can be used to unregister specific scheme
     *
     * @return Set
     */
    public Set<String> schemes() {
        return schemes.keySet();
    }


    /**
     * Associates provided UserStore with resolver.
     * It does not remove previously associated stores.
     *
     * Stores registered earlier have higher priority than registered later.
     *
     * @param store store
     * @return URIResolver
     */
    public URIResolver users(UserStore store) {
        this.store.add(store);
        return this;
    }

    /**
     * Returns UserStore associated with this resolver.
     * If resolver is configured to use more than one store, returned store is
     * composite view with all of them.
     *
     * @return UserStore
     */
    public UserStore users() {
        return store;
    }
    
    private static final class MergeStore implements UserStore {
        
        private final List<UserStore> stores = new RTList<>(new UserStore[]{new ExtractStore()});

        @Override
        public CQuery<GenericMap<String>> find(URI uri) {
            return CQuery.from().collection(stores).flat(s -> s.find(uri));
        }

        @Override
        public CQuery<GenericMap<String>> find(String query) {
            return CQuery.from().collection(stores).flat(s -> s.find(query));
        }
        
        public void add(UserStore store) {
            stores.add(store);
        }
        
    }
    
    private static final class ExtractStore implements UserStore {
        
        @Override
        public CQuery<GenericMap<String>> find(URI uri) {
            GenericMap<String> a = new OpenGenericMap<>();
            a.put(UserProperty.LOGIN, URIUtils.getLogin(uri));
            a.put(UserProperty.PASSWORD, URIUtils.getPassword(uri));
            a.put(UserProperty.SYNTHETIC, true);
            return CQuery.from().value(a);
        }

        @Override
        public CQuery<GenericMap<String>> find(String query) {
            return CQuery.empty();
        }
    
    }

}
