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

package de.rcenvironment.core.communication.internal;

import java.util.HashMap;
import java.util.Map;

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

import de.rcenvironment.core.communication.api.LiveNetworkIdResolutionService;
import de.rcenvironment.core.communication.common.IdentifierException;
import de.rcenvironment.core.communication.common.InstanceNodeSessionId;
import de.rcenvironment.core.communication.common.LogicalNodeId;
import de.rcenvironment.core.communication.common.LogicalNodeSessionId;
import de.rcenvironment.core.communication.common.ResolvableNodeId;
import de.rcenvironment.core.utils.common.StringUtils;
import de.rcenvironment.core.utils.incubator.DebugSettings;

/**
 * Default {@link LiveNetworkIdResolutionService} implementation.
 * 
 * @author Robert Mischke
 */
public class LiveNetworkIdResolutionServiceImpl implements LiveNetworkIdResolutionService {

    private final Map<String, InstanceNodeSessionId> instanceNodeSessionIdMap = new HashMap<>();

    private String localInstanceNodeIdString;

    private String localInstanceNodeSessionIdString;

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

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

    @Override
    public InstanceNodeSessionId resolveInstanceNodeIdStringToInstanceNodeSessionId(String input) throws IdentifierException {
        final InstanceNodeSessionId result;
        synchronized (this) {
            result = instanceNodeSessionIdMap.get(input);
        }
        if (result != null) {
            return result;
        } else {
            throw new IdentifierException("Cannot resolve '" + input
                + "' to a known instance session id; that node has never been reachable, or is not reachable anymore");
        }

    }

    @Override
    public LogicalNodeSessionId resolveToLogicalNodeSessionId(ResolvableNodeId id) throws IdentifierException {
        final LogicalNodeSessionId result;
        result = resolveToLogicalNodeSessionIdInternal(id);
        if (verboseLogging) {
            log.debug(StringUtils.format("Resolved %s %s to %s", id.getType(), id, result));
        }
        return result;
    }

    private LogicalNodeSessionId resolveToLogicalNodeSessionIdInternal(ResolvableNodeId id) throws IdentifierException {
        // note: this method always resolves to the default (latest) session id, even if the parameter is already a session id itself
        final InstanceNodeSessionId resolvedInstanceNodeSessionId =
            resolveInstanceNodeIdStringToInstanceNodeSessionId(id.getInstanceNodeIdString());

        switch (id.getType()) {
        case INSTANCE_NODE_SESSION_ID:
            if (!resolvedInstanceNodeSessionId.isSameInstanceNodeSessionAs((InstanceNodeSessionId) id)) {
                log.debug(StringUtils.format("Resolved a given session id %s to the more recent session id %s for the same instance node",
                    id, resolvedInstanceNodeSessionId));
            }
            // intentional fall-through (no "break")
        case INSTANCE_NODE_ID:
            return resolvedInstanceNodeSessionId.convertToDefaultLogicalNodeSessionId();
        case LOGICAL_NODE_SESSION_ID:
            if (!resolvedInstanceNodeSessionId.getSessionIdPart().equals(((LogicalNodeSessionId) id).getSessionIdPart())) {
                log.debug(StringUtils.format("Resolved a given session id %s to the more recent session id %s for the same instance node",
                    id, resolvedInstanceNodeSessionId));
            }
            // TODO (p3) >8.0.0 could probably done more efficiently
            return ((LogicalNodeSessionId) id).convertToLogicalNodeId().combineWithInstanceNodeSessionId(resolvedInstanceNodeSessionId);
        case LOGICAL_NODE_ID:
            return ((LogicalNodeId) id).combineWithInstanceNodeSessionId(resolvedInstanceNodeSessionId);
        default:
            throw new IllegalStateException();
        }
    }

    /**
     * Registers the local {@link InstanceNodeSessionId} for its instance and instance session ids. This session id is special because it
     * should be protected against any subsequent modification (e.g. by stale information received from other nodes).
     * 
     * @param localINSId the local id to register
     */
    public void registerLocalInstanceNodeSessionId(InstanceNodeSessionId localINSId) {
        synchronized (this) {
            if (localInstanceNodeIdString != null || localInstanceNodeSessionIdString != null) {
                throw new IllegalStateException(); // expect exactly one initialization
            }

            this.localInstanceNodeIdString = localINSId.getInstanceNodeIdString();
            this.localInstanceNodeSessionIdString = localINSId.getInstanceNodeSessionIdString();

            if (instanceNodeSessionIdMap.put(localInstanceNodeIdString, localINSId) != null) {
                throw new IllegalStateException(); // must be the first id to register for this
            }
        }
    }

    /**
     * Registers a new {@link InstanceNodeSessionId} for its instance and instance session ids. If it replaces a previously set
     * {@link InstanceNodeSessionId} for either, log messages are generated.
     * 
     * @param newINSId the new id to register
     */
    public void registerInstanceNodeSessionId(InstanceNodeSessionId newINSId) {

        final String instanceNodeIdString = newINSId.getInstanceNodeIdString();
        final String instanceNodeSessionIdString = newINSId.getInstanceNodeSessionIdString();

        final InstanceNodeSessionId existingResolutionForINId;

        synchronized (this) {

            if (instanceNodeIdString.equals(localInstanceNodeIdString)
                && !instanceNodeSessionIdString.equals(localInstanceNodeSessionIdString)) {
                log.debug(StringUtils.format(
                    "Refused an attempt to replace the local instance's current session id (%s) with another one (%s); "
                        + "this may be caused by stale data received from another instance",
                    localInstanceNodeSessionIdString, instanceNodeSessionIdString));
                if (isAlphabeticallyMoreRecentThan(instanceNodeSessionIdString, localInstanceNodeSessionIdString)) {
                    log.debug(
                        "The conflicting session id received from the network (see above) is more recent than the local instance's "
                            + "session id; this may indicate a duplicate node id within the network, which may result from erronously "
                            + "copying and reusing a profile's internal settings");
                }
                return;
            }

            existingResolutionForINId = instanceNodeSessionIdMap.get(instanceNodeIdString);

            if (existingResolutionForINId == null) {
                instanceNodeSessionIdMap.put(instanceNodeIdString, newINSId);
                log.debug(StringUtils.format(
                    "Registered %s as the first known session id for instance node '%s'", newINSId, instanceNodeIdString));
            } else {
                if (!existingResolutionForINId.equals(newINSId)) {
                    // a new InstanceNodeSessionId became visible before the old one was unregistered
                    if (isAlphabeticallyMoreRecentThan(newINSId.getSessionIdPart(), existingResolutionForINId.getSessionIdPart())) {
                        log.info(StringUtils.format(
                            "Updated the default instance node session id for instance node '%s' from %s to %s; "
                                + "this means that a new session became visible before the old one unregistered from the network "
                                + "(e.g. after a crash or network disconnect)",
                            instanceNodeIdString, existingResolutionForINId, newINSId));
                        instanceNodeSessionIdMap.put(instanceNodeIdString, newINSId);
                    } else {
                        log.debug(StringUtils.format(
                            "Ignored an outdated session id for instance node '%s' (current: %s, ignored: %s) "
                                + "received as part of a remote node's network knowledge",
                            instanceNodeIdString, existingResolutionForINId, newINSId));
                    }
                }
            }
        }
    }

    /**
     * Unregisters a node that has disappeared from the known network.
     * 
     * @param oldINSId the node's session id to unregister
     */
    public void unregisterInstanceNodeSessionId(InstanceNodeSessionId oldINSId) {
        final String instanceNodeIdString = oldINSId.getInstanceNodeIdString();
        synchronized (this) {
            final InstanceNodeSessionId existingResolutionForINId = instanceNodeSessionIdMap.get(instanceNodeIdString);
            if (existingResolutionForINId != null) {
                if (existingResolutionForINId.isSameInstanceNodeSessionAs(oldINSId)) {
                    // the single known session id was unregistered; no known node-to-session resolution anymore
                    // TODO (p2) potential error case: an outdated id could be registered again after this; keep track of all active ids?
                    instanceNodeSessionIdMap.remove(instanceNodeIdString);
                } else {
                    // note: this is a very technical message for the INFO log level, but it should be visible to indicate that the previous
                    // WARNING log message has been resolved
                    log.debug(StringUtils.format(
                        "The outdated instance node session id %s for instance node '%s' has been properly unregistered now",
                        oldINSId, instanceNodeIdString));
                }
            }
        }
    }

    private boolean isAlphabeticallyMoreRecentThan(final String id1, String id2) {
        return id1.compareTo(id2) > 0;
    }

}
