package de.renew.plugin;


import java.lang.module.ModuleDescriptor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import de.renew.plugin.jpms.ModuleLayerListener;

/**
 * The ServiceLookupInfrastructure (SLI) manages the service interfaces and providers.
 * For this purpose, the modules of a module layer are transferred to the SLI, which searches them for service interfaces and providers.
 * The interfaces and providers found are then managed in the SLI.
 * Removing the interfaces and providers is analogous to this.
 * <br>
 * The SLI can be used by passing it a service interface as a class.
 * The SLI will return found service provider(s) for the given service interface.
 *
 * @author Laif-Oke Clasen
 * @author Kjell Ehlers
 * @version 1.2
 */
public class ServiceLookupInfrastructure {

    private static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(ServiceLookupInfrastructure.class);

    // A map from the service interfaces to the module layers of the service providers
    private final Map<Class<?>, List<ModuleLayer>> _providerLocations = new ConcurrentHashMap<>();
    // A map from the service interface string to the service interface
    private final Map<String, Class<?>> _serviceInterfaces = new HashMap<>();
    // A map of modules to a list of service interface strings contained in the module
    private final Map<Module, List<String>> _serviceInterfaceLocations = new HashMap<>();

    private static final ServiceLookupInfrastructure SLI = new ServiceLookupInfrastructure();

    private ServiceLookupInfrastructure() {

    }

    /**
     * Returns this instance of ServiceLookupInfrastructure
     * @return This serviceLookupInfrastructure
     */
    public static ServiceLookupInfrastructure getInstance() {
        return SLI;
    }

    /**
     * Adds the service providers and the service interfaces of the given modules to the service lookup infrastructure.
     * @param modules The service provider and service interface modules to be added to the service lookup infrastructure.
     * @author Laif-Oke Clasen
     */
    void addServicesOfModules(Set<Module> modules) {
        for (Module module : modules) {
            addServiceInterfacesOfModule(module);
            addServiceProvidersOfModule(module);
        }
    }

    /**
     * Loads the first available implementation for a given service.
     * <p>
     * The search is conducted in a depth-first manner, traversing all module
     * layers known to the SLI and their parents:
     * <ul>
     *     <li>For each layer known to the SLI a full traversal of the layer
     *     and all its parent layers is done before continuing with the next
     *     known layer.</li>
     *     <li>All modules inside a layer are traversed before continuing with
     *     the next layer. The ordering of these modules is not defined.</li>
     * </ul>
     * The available providers are then lazily loaded and instantiated, with the
     * first one being returned.
     * <p>
     * Note: Providers in unnamed modules are not located.
     *
     * @param service   The interface or abstract class representing the service
     * @param <S>       The class of the service type
     * @return          An implementation for the service
     * @throws ServiceLookupException   If no such implementation could be found
     */
    public <S> S getFirstServiceProvider(Class<S> service) throws ServiceLookupException {
        List<ModuleLayer> locations = getLocationsOfServiceProvider(service);
        if (!locations.isEmpty()) {
            ensureAccess(service);
        }

        for (ModuleLayer layer : locations) {
            var loader = ServiceLoader.load(layer, service);
            var it = loader.iterator();
            while (it.hasNext()) {
                try {
                    return it.next();
                } catch (ServiceConfigurationError e) {
                    LOGGER.warn(
                        "Couldn't load service provider due to a " + "configuration error.", e);

                    // Try the next provider
                }
            }
        }
        throw new ServiceLookupException(service);
    }

    /**
     * Loads all available implementations for a given service.
     * <p>
     * The search is conducted in a depth-first manner, traversing all module
     * layers known to the SLI and their parents:
     * <ul>
     *     <li>For each layer known to the SLI a full traversal of the layer
     *     and all its parent layers is done before continuing with the next
     *     known layer.</li>
     *     <li>All modules inside a layer are traversed before continuing with
     *     the next layer. The ordering of these modules is not defined.</li>
     * </ul>
     * The available providers are then lazily loaded and instantiated.
     * <p>
     * Note: Providers in unnamed modules are not located.
     *
     * @param service   The interface or abstract class representing the service
     * @param <S>       The class of the service type
     * @return          All available implementations for the service
     */
    public <S> Collection<S> getAllServiceProvider(Class<S> service) {
        List<ModuleLayer> locations = getLocationsOfServiceProvider(service);
        if (!locations.isEmpty()) {
            ensureAccess(service);
        }

        // Only add each service provider once when traversing the module layers
        Set<S> services = new HashSet<>();
        for (ModuleLayer layer : locations) {
            var loader = ServiceLoader.load(layer, service);
            var it = loader.iterator();
            while (it.hasNext()) {
                try {
                    services.add(it.next());
                } catch (ServiceConfigurationError e) {
                    LOGGER.warn(
                        "Couldn't load service provider due to a " + "configuration error.", e);

                    // Try the next provider
                }
            }
        }
        return services;
    }

    // Needed for invoking a ServiceLoader on behalf of other modules
    private void ensureAccess(Class<?> service) {
        Module loader = getClass().getModule();
        if (!loader.canUse(service)) {
            loader.addUses(service);
        }
    }

    private Set<String> extractServiceStringsOfModule(Module module) {
        Set<String> usesServiceStrings = module.getDescriptor().uses();
        Set<ModuleDescriptor.Provides> provServiceStrings = module.getDescriptor().provides();

        // Combines the service Strings of the provides- and uses-entries in the module info.
        Set<String> serviceStrings = new HashSet<>();
        for (ModuleDescriptor.Provides serviceString : provServiceStrings) {
            serviceStrings.add(serviceString.service());
        }
        serviceStrings.addAll(usesServiceStrings);
        return serviceStrings;
    }

    private void addServiceInterfacesOfModule(Module module) {

        Set<String> serviceStrings = extractServiceStringsOfModule(module);

        // For each service string it is checked whether the service interface and the location of the service
        // interface are already included in the service lookup infrastructure.
        for (String serviceString : serviceStrings) {
            try {
                Class<?> serviceInterface = module.getClassLoader().loadClass(serviceString);
                Module serviceInterfaceModule = serviceInterface.getModule();

                if (!_serviceInterfaces.containsKey(serviceString)) {
                    _serviceInterfaces.put(serviceString, serviceInterface);
                }

                if (_serviceInterfaceLocations.containsKey(serviceInterfaceModule)) {
                    List<String> value = _serviceInterfaceLocations.get(serviceInterfaceModule);
                    if (!value.contains(serviceString)) {
                        value.add(serviceString);
                        _serviceInterfaceLocations.put(serviceInterfaceModule, value);
                    }
                } else {
                    List<String> value = new ArrayList<>();
                    value.add(serviceString);
                    _serviceInterfaceLocations.put(serviceInterfaceModule, value);
                }
            } catch (ClassNotFoundException e) {
                LOGGER.warn(
                    "The service interface of the service provider " + serviceString
                        + " is not found.",
                    e);
            }
        }
    }



    private void addServiceProvidersOfModule(Module providerModule) {
        Set<ModuleDescriptor.Provides> providedServices = providerModule.getDescriptor().provides();
        ModuleLayer providerLayer = providerModule.getLayer();

        // For every "provide" statement in the module-info the service interface is mapped to the layer of the
        // service provider.
        for (ModuleDescriptor.Provides providedService : providedServices) {
            String service = providedService.service();
            Class<?> serviceInterface = _serviceInterfaces.get(service);

            // If the Key doesn't exist a new key-value-tuple is added to the map.
            // else the value of the key-value-tuple is updated.
            if (!_providerLocations.containsKey(serviceInterface)) {
                List<ModuleLayer> value = new ArrayList<>();
                value.add(providerLayer);
                _providerLocations.put(serviceInterface, value);
            } else {
                List<ModuleLayer> oldValue = _providerLocations.get(serviceInterface);
                List<ModuleLayer> newValue = _providerLocations.get(serviceInterface);
                newValue.add(providerLayer);
                _providerLocations.replace(serviceInterface, oldValue, newValue);
            }
        }
    }

    /**
     * Returns a list of module layers.
     * These module layers contain the service provider for the given serviceInterface.
     *
     * @param serviceInterface  The service interface as a class.
     * @return List of the service providers' module layers.
     * @author Laif-Oke Clasen
     * @author Kjell Ehlers
     */
    private List<ModuleLayer> getLocationsOfServiceProvider(Class<?> serviceInterface) {
        // Never return null
        return _providerLocations.getOrDefault(serviceInterface, List.of());
    }

    /**
     * Removes the service providers and the service interfaces of the given modules from the service lookup infrastructure.
     * @param modules The service provider and the service interfaces of the modules that should be removed from the service lookup infrastructure.
     * @author Laif-Oke Clasen
     */
    synchronized void removeServicesOfModules(Set<Module> modules) {
        for (Module module : modules) {
            removeServiceProvidersOfModule(module);
            removeServiceInterfacesOfModule(module);

        }
    }

    private synchronized void removeServiceInterfacesOfModule(Module providerModule) {
        if (_serviceInterfaceLocations.containsKey(providerModule)) {
            for (String service : _serviceInterfaceLocations.get(providerModule)) {
                _providerLocations.remove(_serviceInterfaces.get(service));
                _serviceInterfaces.remove(service);
            }
            _serviceInterfaceLocations.remove(providerModule);
        }
    }


    private synchronized void removeServiceProvidersOfModule(Module providerModule) {
        Set<ModuleDescriptor.Provides> providedServices = providerModule.getDescriptor().provides();
        ModuleLayer providerLayer = providerModule.getLayer();

        // For every "provide" statement in the module-info the mapping form the service interface to the service
        // provider module layer is removed
        for (ModuleDescriptor.Provides providedService : providedServices) {
            String service = providedService.service();
            Class<?> serviceInterface = _serviceInterfaces.get(service);
            List<ModuleLayer> value = null;
            if (serviceInterface != null) {
                value = _providerLocations.get(serviceInterface);
            }

            // The service interface is already removed, but some providers are still in the system
            if (value == null) {
                for (var entry : _providerLocations.entrySet()) {
                    if (entry.getKey().toString().contains(service)) {
                        _providerLocations.remove(entry.getKey());
                    }
                }
            }
            // If the list of the service providers' module layer is bigger than 1, than the mapping entry is updated.
            // Else the mapping entry is removed.
            else if (value.size() > 1) {
                List<ModuleLayer> oldValue = _providerLocations.get(serviceInterface);
                value.remove(providerLayer);
                _providerLocations.replace(serviceInterface, oldValue, value);
            } else if (value.size() == 1) {
                _providerLocations.remove(serviceInterface);
            }
        }
    }

    final class LayerServicesListener implements ModuleLayerListener {
        @Override
        public void layerAdded(Set<Module> modules) {
            addServicesOfModules(modules);
        }

        @Override
        public void layerRemoved(Set<Module> modules) {
            removeServicesOfModules(modules);
        }
    }
}
