/*
 * Copyright 2006-2020 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */

package de.rcenvironment.core.communication.testutils;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import de.rcenvironment.core.utils.incubator.DebugSettings;
import de.rcenvironment.toolkit.utils.common.AutoCreationMap;

/**
 * An helper class to set up, bind and activate OSGi-DS service instances for integration testing. Note that only dependency binding and
 * service activation is supported; unbinding and deactivation is not.
 * 
 * @author Robert Mischke
 */
public class VirtualServiceRegistry {

    private List<VirtualService> unboundServices = new ArrayList<VirtualServiceRegistry.VirtualService>();

    private AutoCreationMap<Class<?>, List<VirtualService>> activatedServices =
        new AutoCreationMap<Class<?>, List<VirtualService>>() {

            @Override
            protected List<VirtualService> createNewEntry(Class<?> key) {
                return new ArrayList<VirtualService>();
            }
        };

    private final boolean verboseLogging = DebugSettings.getVerboseLoggingEnabled(getClass());

    private final Log log = LogFactory.getLog(getClass());

    /**
     * Encapsulation of a registered service instance.
     * 
     * @author Robert Mischke
     */
    private final class VirtualService {

        private final Object implementation;

        private final boolean expectActivator;

        private final Class<?>[] serviceClasses;

        private final Map<Class<?>, Method> bindMethods = new HashMap<Class<?>, Method>();

        VirtualService(Object implementation, boolean expectActivator, Class<?>[] serviceClasses) {
            this.implementation = implementation;
            this.expectActivator = expectActivator;
            this.serviceClasses = serviceClasses;
            final Method[] methods = implementation.getClass().getMethods();
            for (Method method : methods) {
                if (method.getName().startsWith("bind") && method.getParameterTypes().length == 1) {
                    Class<?> type = method.getParameterTypes()[0];
                    if (verboseLogging) {
                        log.debug("Found bind method: " + implementation.getClass().getName() + "." + method.getName() + "() -> "
                            + type.getName());
                    }
                    bindMethods.put(type, method);
                }
            }
        }

        public Class<?>[] getServiceClasses() {
            return serviceClasses;
        }

        public Object getImplementation() {
            return implementation;
        }

        public Map<Class<?>, Method> getBindMethods() {
            return bindMethods;
        }

    }

    /**
     * Registers an already-initialized (activated) service instance. Used to provide external service instances to managed instances.
     * 
     * @param implementation the service implementation
     * @param serviceClasses the service interfaces to register this implementation for
     */
    public void registerProvidedService(Object implementation, Class<?>... serviceClasses) {
        final VirtualService service = new VirtualService(implementation, false, serviceClasses);
        for (Class<?> clazz : serviceClasses) {
            activatedServices.get(clazz).add(service);
        }
    }

    /**
     * Registers an unbound service instance. This is the main method to register services by.
     * 
     * The given service instance is expected to have an activator method; if it does not, use the method variant with a boolean parameter
     * to prevent a runtime exception.
     * 
     * This is a convenience shortcut for calling {@link #registerManagedService(Object, true, Class...)}.
     * 
     * @param implementation the service implementation
     * @param serviceClasses the service interfaces to register this implementation for
     */
    public void registerManagedService(Object implementation, Class<?>... serviceClasses) {
        registerManagedService(implementation, true, serviceClasses);
    }

    /**
     * Registers an unbound service instance. This is the main method to register services by.
     * 
     * @param implementation the service implementation
     * @param expectActivator true if this service is expected to have an activator; if this expectation is not met either way, an runtime
     *        exception is thron. logged
     * @param serviceClasses the service interfaces to register this implementation for
     */
    public void registerManagedService(Object implementation, boolean expectActivator, Class<?>... serviceClasses) {
        final VirtualService service = new VirtualService(implementation, expectActivator, serviceClasses);
        unboundServices.add(service);
    }

    /**
     * Returns a service that was registered for the given interface class, if it exists.
     * 
     * @param <T> the service interface class (for generics)
     * @param clazz the service interface class
     * @return a service instance that was registered for the given interface, or null if none exists
     */
    @SuppressWarnings("unchecked")
    public <T> T getService(Class<T> clazz) {
        final List<VirtualService> list = activatedServices.get(clazz);
        if (list.isEmpty()) {
            return null;
        }
        // TODO check for uniqueness?
        return (T) list.get(0).getImplementation();
    }

    /**
     * Returns all services that were registered for the given interface class.
     * 
     * TODO apply generics on return value?
     * 
     * @param clazz the service interface class
     * @return all service instances that were registered for the given interface; may be empty
     */
    public Collection<?> getServices(Class<?> clazz) {
        return new ArrayList<Object>();
    }

    /**
     * Attempts to bind dependencies of managed services with already-activated or externally provided services, and then activate them,
     * until no more change is possible.
     * 
     * TODO add "all services activated" return value?
     */
    public void bindAndActivateServices() {
        VirtualService activatedService;
        do {
            activatedService = null;
            for (VirtualService service : unboundServices) {
                boolean satisfied = checkDependencies(service);
                if (satisfied) {
                    bindDependencies(service);
                    activate(service);
                    activatedService = service;
                    break;
                }
            }
            if (activatedService != null) {
                unboundServices.remove(activatedService);
                for (Class<?> serviceClass : activatedService.getServiceClasses()) {
                    activatedServices.get(serviceClass).add(activatedService);
                }
            }

        } while (activatedService != null);
        if (!unboundServices.isEmpty()) {
            log.warn("After bindAndActivateServices(), there are still unbound services left:");
            for (VirtualService service : unboundServices) {
                log.warn("  " + service.getImplementation().getClass());
                for (Class<?> depClass : service.getBindMethods().keySet()) {
                    String warningSuffix = "";
                    if (getService(depClass) == null) {
                        warningSuffix = " [unsatisfied]";
                    }
                    log.warn("    -> depends on " + depClass.getName() + warningSuffix);
                }
                for (Class<?> serviceClass : service.getServiceClasses()) {
                    log.warn("    <- provides " + serviceClass.getName());
                }
            }
            throw new RuntimeException("Failed to bind and activate all services; see log output for details");
        }
    }

    private boolean checkDependencies(VirtualService service) {
        boolean satisfied = true;
        for (Map.Entry<Class<?>, Method> bindMethod : service.getBindMethods().entrySet()) {
            final Class<?> type = bindMethod.getKey();
            if (getService(type) == null) {
                satisfied = false;
                // log.debug("Service " + service.getImplementation().getClass().getName()
                // + " has unsatisfied dependency, waiting for " + type.getName());
                break;
            }
        }
        return satisfied;
    }

    private void bindDependencies(VirtualService service) {
        if (verboseLogging) {
            log.debug("Dependencies of service " + service.getImplementation().getClass().getName() + " are satisfied, starting to bind");
        }
        for (Map.Entry<Class<?>, Method> bindMethod : service.getBindMethods().entrySet()) {
            final Class<?> type = bindMethod.getKey();
            final Object dependency = getService(type);
            if (verboseLogging) {
                log.debug("Binding dependency of service " + service.getImplementation().getClass().getName()
                    + " with instance of type " + dependency.getClass().getName());
            }
            Exception error = null;
            try {
                bindMethod.getValue().invoke(service.getImplementation(), dependency);
            } catch (IllegalArgumentException e) {
                error = e;
            } catch (IllegalAccessException e) {
                error = e;
            } catch (InvocationTargetException e) {
                error = e;
            }
            if (error != null) {
                throw new RuntimeException("Error calling bind method", error);
            }
        }
    }

    private void activate(VirtualService service) {
        Exception error = null;
        final Object implementation = service.getImplementation();
        Class<? extends Object> implementationClass = implementation.getClass();
        try {
            try {
                // TODO support activate(BundleContext) as well; requires definition of a virtual BundleContext first, though - misc_ro
                final Method noArgs = implementationClass.getMethod("activate", new Class<?>[0]);
                if (!service.expectActivator) {
                    throw new IllegalArgumentException("Activator found in " + implementationClass.getName()
                        + ", but it was specified to not have one");
                }
                if (verboseLogging) {
                    log.debug("Activating service " + implementationClass.getName());
                }
                noArgs.invoke(implementation);
            } catch (NoSuchMethodException e) {
                if (service.expectActivator) {
                    throw new IllegalArgumentException("No activator method found in " + implementationClass.getName()
                        + ", but it was specified to have one");
                }
                return;
            }
        } catch (IllegalArgumentException e) {
            error = e;
        } catch (SecurityException e) {
            error = e;
        } catch (IllegalAccessException e) {
            error = e;
        } catch (InvocationTargetException e) {
            error = e;
        }
        if (error != null) {
            throw new RuntimeException("Error activating service", error);
        }
    }
}
