package net.ranides.assira.reflection.util;

import net.ranides.assira.generic.Ref;
import org.slf4j.Logger;

import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.functional.checked.CheckedFunction;
import net.ranides.assira.text.Charsets;
import net.ranides.assira.text.IOStrings;
import net.ranides.assira.text.StringTraits;
import net.ranides.assira.text.StringUtils;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.util.*;
import java.util.function.Consumer;

/**
 * This is manual reimplementation of ServiceLoader.
 * It reads content from "META-INF/services/".
 *
 * @param <S> S
 */
public class ServiceScanner<S> {

    private static final String PREFIX = "META-INF/services/";

    private final Class<S> service;

    private ServiceResolver<S> resolver;

    private Consumer<Throwable> listener;

    private ClassLoader loader;

    private ServiceScanner(Class<S> service) {
        this.service = service;
        this.loader = ClassLoader.getSystemClassLoader();
        this.resolver = this::resolvePublic;
        this.listener = error -> { };
    }

    /**
     * Creates new scanner which searches for all implementations of "service" type.
     * It uses system classloader.
     *
     * @param service service
     * @param <S> S
     * @return ServiceScanner
     */
    public static <S> ServiceScanner<S> scanner(Class<S> service) {
        return new ServiceScanner<>(service);
    }

    /**
     * Configures scanner to use provided classloader instead of system.
     *
     * @param loader loader
     * @return this
     */
    public ServiceScanner<S> loader(ClassLoader loader) {
        this.loader = loader;
        return this;
    }

    /**
     * Configures scanner to return non-public services too.
     *
     * @return this
     */
    public ServiceScanner<S> resolvePrivate() {
        return resolver(this::resolvePrivate);
    }

    /**
     * Configures scanner to use custom service resolver.
     * Resolver receives classname and should return service instance.
     *
     * It can be used for example, if we want to create services using other than default constructor.
     *
     * Scanner remembers all previously configured resolvers and calls them one after another.
     *
     * @param handler handler
     * @return this
     */
    public ServiceScanner<S> resolver(ServiceResolver<S> handler) {
        ServiceResolver<S> prev = this.resolver;
        this.resolver = name -> {
            try {
                Ref<S> resolved = handler.apply(name);
                if(resolved.isPresent()) {
                    return resolved;
                }
            } catch(ReflectiveOperationException cause) {
                listener.accept(cause);
            }
            return prev.apply(name);
        };
        return this;
    }

    /**
     * Configures scanner to call provided handler if it was impossible to create discovered service.
     *
     * Scanner remembers all previously configured handlers and calls them one after another.
     *
     * @param handler handler
     * @return this
     */
    public ServiceScanner<S> listener(Consumer<Throwable> handler) {
        this.listener = listener.andThen(handler);
        return this;
    }

    /**
     * Configures scanner to log information if it was impossible to create discovered service.
     *
     * Scanner remembers all previously configured loggers and calls them one after another.
     *
     * @param logger logger
     * @return this
     */
    public ServiceScanner<S> logger(Logger logger) {
        this.listener = listener.andThen(error -> logger.error("ServiceScanner error", error));
        return this;
    }

    /**
     * Returns query with all discovered service instances.
     *
     * @return CQuery
     */
    public CQuery<S> services() {
        return names().mapOptional(type -> makeInstance(type));
    }

    /**
     * Returns query with all discovered service names (class names).
     *
     * @return CQuery
     */
    public CQuery<String> names() {
        String fqn = PREFIX + service.getName();
        return CQuery.from()
            .enumeration(() -> findFiles(fqn))
            .flatIterable(this::loadLines)
            .mapOptional(this::parseLine)
            .distinct();

    }

    private Enumeration<URL> findFiles(String fqn) throws IOException {
        return loader == null ? ClassLoader.getSystemResources(fqn) : loader.getResources(fqn);
    }

    private List<String> loadLines(URL url) {
        try {
            return IOStrings.readLines(url.openStream(), Charsets.UTF8);
        } catch (IOException e) {
            listener.accept(new IllegalArgumentException("Invalid URL " + url.toString(), e));
            return Collections.emptyList();
        }
    }

    private Ref<String> parseLine(String line) {
        int end = StringUtils.indexOf(line, "#");
        String name = StringUtils.trim(end>0 ? StringUtils.substr(line, end) : line);

        if(StringTraits.isEmpty(name)) {
            return Ref.empty();
        }

        if(name.indexOf(' ')>=0 || name.indexOf('\t')>=0) {
            listener.accept(new IllegalArgumentException("Illegal configuration-file syntax " + line));
            return Ref.empty();
        }

        if(!StringTraits.isJavaIdentifier(name)) {
            listener.accept(new IllegalArgumentException(line + "Illegal provider-class name"));
            return Ref.empty();
        }

        return Ref.of(name);
    }

    private Ref<S> resolvePublic(String name) throws ReflectiveOperationException {
        return Ref.of(service.cast(Class.forName(name, false, loader).newInstance()));
    }

    private Ref<S> resolvePrivate(String name) throws ReflectiveOperationException {
        Constructor<?> ctor = Class.forName(name, false, loader).getConstructor();
        if(!MemberTraits.isPublic(ctor)) {
            ctor.setAccessible(true);
        }
        return Ref.of(service.cast(ctor.newInstance()));
    }

    private Ref<S> makeInstance(String name) {
        try {
            return resolver.apply(name);
        } catch(ReflectiveOperationException cause) {
            listener.accept(cause);
        }
        return Ref.empty();
    }

    /**
     * Custom ServiceResolver is used to create service instances in other way than by calling default constructor.
     *
     * @param <S> S
     */
    public interface ServiceResolver<S> extends CheckedFunction<String, Ref<S>, ReflectiveOperationException> { }

}
