package de.joshavg.simpledic;

import de.joshavg.simpledic.exception.ClassNotRegistered;
import de.joshavg.simpledic.exception.ContainerInitException;
import de.joshavg.simpledic.exception.SdicInstantiationException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The main container that loads a specific properties file
 * and creates instances of defined services
 * <p>
 * Services are given arbitrary names and can be defined as singletons:
 * <pre><code>
 * service.servicename: tld.vendor.project.ServiceClass
 * service.servicename.singleton: true
 * </code></pre>
 * Service names must match the regular expression <code>^service\.[^.]+$</code>
 * <p>
 * Services declare their dependencies via their constructor. Only one
 * constructor per class is allowed.
 * All dependencies must be declared in the same container as the declaring
 * service.
 * <p>
 * After loading the properties file, an {@link IntegrityCheck} will be performed.
 */
public class SdiContainer implements SdiContainerInterface {

    private static final Logger LOG = LoggerFactory.getLogger(SdiContainer.class);

    private final List<ServiceDefinition> definitions;
    private final Map<ServiceDefinition, Object> singletons;

    private SdiContainer(List<ServiceDefinition> definitions) {
        this.definitions = definitions;
        this.singletons = new HashMap<>();
    }

    /**
     * Creates a Container using the file <code>sdic.properties</code> from the classpath.
     *
     * @return the loaded and integrity checked container
     */
    @SuppressWarnings("unused")
    public static SdiContainer load() {
        return load("sdic.properties");
    }

    /**
     * Creates a Container using the properties file at the designated location
     *
     * @param filename the filename that shall be loaded
     * @return the loaded and integrity checked container
     */
    @SuppressWarnings("WeakerAccess")
    public static SdiContainer load(String filename) {
        Properties props = new Properties();

        InputStream inputStream = SdiContainer.class.getClassLoader().getResourceAsStream(filename);
        if (inputStream == null) {
            throw new ContainerInitException("config file " + filename + " not found", null);
        }

        try {
            props.load(inputStream);
        } catch (IOException e) {
            throw new ContainerInitException("failed loading properties", e);
        }

        IntegrityCheck integrityCheck = new IntegrityCheck(props);
        integrityCheck.check();
        return new SdiContainer(integrityCheck.getDefinitions());
    }

    private <T> ServiceDefinition getDefinition(Class<T> clz) {
        return definitions.stream()
            .filter(d -> d.getClz() == clz)
            .findFirst()
            .orElse(null);
    }

    /**
     * Creates and returns a service instance of the given class
     * <p>
     * Dependency services are automatically created. Services marked
     * as singletons will be only created once, either as transient
     * or direct dependency.
     *
     * @param clz the type which shall be created
     * @return the created type with declared dependencies fulfilled
     */
    @Override
    public <T> T getInstance(Class<T> clz) {
        LOG.trace("instance ordered: ", clz);
        ServiceDefinition definition = getDefinition(clz);
        if (definition == null) {
            throw new ClassNotRegistered(clz);
        }

        LOG.debug("service name is {}", definition.getName());

        if (isStoredAsSingleton(definition)) {
            return clz.cast(singletons.get(definition));
        }

        try {
            T instance = new Instantiator<>(clz, this).createInstance();
            handleSingleton(definition, instance);
            return instance;
        } catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
            throw new SdicInstantiationException(e);
        }
    }

    /**
     * returns all registered services that implement or extend the given class
     *
     * @param clz parent class or interface
     * @param T type of the requested service
     * @return the list with instances, or an empty list if nothing is found
     */
    @SuppressWarnings("WeakerAccess")
    public <T> List<T> getInstancesThatImplement(Class<T> clz) {
        LOG.trace("instances of interface {} ordered", clz);
        return definitions.stream()
            .filter(d -> clz.isAssignableFrom(d.getClz()))
            .map(ServiceDefinition::getClz)
            .map(c -> clz.cast(getInstance(c)))
            .collect(Collectors.toList());
    }

    private <T> void handleSingleton(ServiceDefinition definition, T instance) {
        if (definition.isSingleton()) {
            singletons.put(definition, instance);
        }
    }

    private boolean isStoredAsSingleton(ServiceDefinition def) {
        return singletons.containsKey(def);
    }

}