/*
 * Copyright 2006-2019 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * http://www.rcenvironment.de/
 */

package de.rcenvironment.core.communication.common;

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

import de.rcenvironment.core.communication.api.NodeIdentifierService;

/**
 * A static holder used to provide a required service instance to (potentially hidden/transitive) deserialiation calls of
 * {@link CommonIdBase} subclasses.
 * 
 * @author Robert Mischke
 */
public final class NodeIdentifierContextHolder {

    private static final String SERVICE_CLASS_NAME = NodeIdentifierService.class.getSimpleName();

    private static final transient ThreadLocal<NodeIdentifierService> sharedServiceThreadLocal = new ThreadLocal<>();

    private static volatile NodeIdentifierService defaultNodeIdentifierService;

    private static final Log sharedLog = LogFactory.getLog(NodeIdentifierContextHolder.class);

    public NodeIdentifierContextHolder() {}

    /**
     * Sets the service instance that should be used for deserializing id objects within the current {@link Thread}. Using this approach
     * instead of a singleton is important for proper unit/integration testing.
     * 
     * @param serviceInstance the service instance to set
     * @return the former service instance, if any; useful for restoring a previous context after an operation
     */
    public static NodeIdentifierService setDeserializationServiceForCurrentThread(NodeIdentifierService serviceInstance) {
        NodeIdentifierService previous = sharedServiceThreadLocal.get();
        sharedServiceThreadLocal.set(serviceInstance);
        return previous;
    }

    /**
     * Retrieves the service instance that should be used for deserializing id objects within the current {@link Thread}. Using this
     * approach instead of a singleton is important for proper unit/integration testing.
     * 
     * @return the service instance set for the current {@link Thread}
     */
    public static NodeIdentifierService getRawDeserializationServiceForCurrentThread() {
        return sharedServiceThreadLocal.get();
    }

    /**
     * Retrieves the service instance that should be used for deserializing id objects within the current {@link Thread}. Using this
     * approach instead of a singleton is important for proper unit/integration testing.
     * 
     * @return the service instance set for the current {@link Thread}
     */
    public static NodeIdentifierService getDeserializationServiceForCurrentThread() {
        final NodeIdentifierService result = sharedServiceThreadLocal.get();
        if (result == null) {
            if (defaultNodeIdentifierService != null) {
                // TODO >8.0 switch to ThreadContext once it is available in all threading contexts
                // sharedLog.debug("Using the global default " + SERVICE_CLASS_NAME + " instance; consider setting this explicitly through "
                // + NodeIdentifierContextHolder.class.getSimpleName());
                return defaultNodeIdentifierService;
            }
            // TODO stopgap solution until a more generic approach is implemented
            NodeIdentifierService testNodeIdentifierService = NodeIdentifierTestUtils.getTestNodeIdentifierService();
            if (testNodeIdentifierService != null) {
                sharedLog.warn("There is no " + SERVICE_CLASS_NAME
                    + " instance registered for the current thread; falling back to the default test instance");
                return testNodeIdentifierService;
            }
            throw new IllegalStateException(
                "There is no " + SERVICE_CLASS_NAME
                    + " instance registered for the current thread, which is required to deserialize distributed node identifiers, "
                    + "and there is no global default instance (typical in test contexts); use "
                    + NodeIdentifierContextHolder.class.getName() + " methods to fix this");
        }
        return result;
    }

    /**
     * OSGi-DS bind method; the "live" service instance is created and injected here by OSGi-DS. This service instance is used as a fallback
     * when the current thread has no explicit service set. This prevents live code from failing in rarely-used code parts.
     * 
     * In integration testing, no such default instance is available, which causes an exception to be thrown. This is intentional to locate
     * these places and add explicit thread context setting there.
     * 
     * @param newInstance the new service instance
     */
    public synchronized void bindNodeIdentifierService(NodeIdentifierService newInstance) {
        if (defaultNodeIdentifierService != null) {
            // ensure only one static service is ever called for consistency
            throw new IllegalStateException("Tried to set a NodeIdentifierService instance, but it was already defined");
        }
        defaultNodeIdentifierService = newInstance;
        LogFactory.getLog(NodeIdentifierContextHolder.class).debug("Injected live " + newInstance.getClass() + " instance");
    }

    /**
     * Transitional method to prevent CheckStyle from complaining that this method should have a private constructor, which must however be
     * public to allow static OSGi-DS injection (sigh...). This will disappear once no more code uses this factory outside unit/integration
     * tests.
     */
    public void transitionalDummy() {}
}
