/*
 * Copyright 2006-2021 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */
package de.rcenvironment.core.communication.transport.jms.common;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

import javax.jms.ConnectionFactory;
import javax.jms.ExceptionListener;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.ObjectMessage;
import javax.jms.Queue;
import javax.jms.Session;

import de.rcenvironment.core.communication.channel.MessageChannelState;
import de.rcenvironment.core.communication.common.CommunicationException;
import de.rcenvironment.core.communication.common.InstanceNodeSessionId;
import de.rcenvironment.core.communication.transport.spi.BrokenMessageChannelListener;
import de.rcenvironment.core.communication.transport.spi.HandshakeInformation;
import de.rcenvironment.core.communication.transport.spi.MessageChannelEndpointHandler;
import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils;
import de.rcenvironment.core.utils.common.StringUtils;

/**
 * Represents a self-initiated JMS connection, ie a connection that was established from the local node to a remote node.
 * 
 * @author Robert Mischke
 */
public class SelfInitiatedJmsMessageChannel extends AbstractJmsMessageChannel {

    private static final int INITIAL_HANDSHAKE_TIMEOUT_MSEC = 15 * 1000;

    private ConnectionFactory connectionFactory;

    private BrokenMessageChannelListener brokenConnectionListener;

    // private TemporaryQueue remoteInitiatedRequestInboxQueue;

    public SelfInitiatedJmsMessageChannel(InstanceNodeSessionId localNodeId, ConnectionFactory connectionFactory,
        BrokenMessageChannelListener brokenConnectionListener) {
        super(localNodeId);
        this.connectionFactory = connectionFactory;
        this.brokenConnectionListener = brokenConnectionListener;
    }

    void connectToJmsBroker() throws JMSException {
        connection = connectionFactory.createConnection();
        connection.setExceptionListener(new ExceptionListener() {

            @Override
            public void onException(JMSException exception) {
                log.warn(StringUtils.format("Asynchronous JMS exception in outgoing connection %s: %s ", getChannelId(),
                    exception.toString()));
                // DO NOT automatically assume a broken connection on an async exception; this is covered by "health checking" - misc_ro
                // brokenConnectionListener.onChannelBroken(SelfInitiatedJmsMessageChannel.this);
            }
        });
        connection.start();
    }

    @Override
    protected void onClosedOrBroken() {
        log.debug("Closing self-initiated channel " + getChannelId());
        super.onClosedOrBroken();

        final boolean isActiveShutdown = getState() == MessageChannelState.CLOSED;

        log.debug("Triggering asynchronous JMS disconnect of message channel " + getChannelId());
        threadPool.execute("JMS Network Transport: Asynchronous disconnect", getChannelId(), () -> tearDownJmsConnection(isActiveShutdown));
    }

    private void tearDownJmsConnection(boolean isActiveShutdown) {
        // on a clean shutdown, send "goodbye" message and wait
        if (isActiveShutdown) {
            sendShutdownMessageToRemoteRequestInbox();
            try {
                Thread.sleep(JmsProtocolConstants.WAIT_AFTER_SENDING_SHUTDOWN_MESSAGE_MSEC);
            } catch (InterruptedException e1) {
                // log and proceed; still try to close the connection
                log.warn("Interrupted between sending the shutdown notice and closing the JMS connection");
            }
        }

        try {
            // note: this should automatically clean up (discard) the remoteInitiatedRequestInboxQueue
            if (connection != null) {
                connection.close();
            } else {
                log.debug("No JMS connection for channel " + getChannelId() + " when asked to tear it down");
            }
        } catch (JMSException e) {
            log.debug("Exception while closing JMS connection", e);
        }
    }

    HandshakeInformation performInitialHandshake(JMSHandshakeInformation ownHandshakeInformation,
        MessageChannelEndpointHandler remoteInitiatedConnectionEndpointHandler) throws JMSException,
        CommunicationException, TimeoutException, IOException {
        Session initialSession = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
        try {
            Queue initialInbox = initialSession.createQueue(JmsProtocolConstants.QUEUE_NAME_INITIAL_BROKER_INBOX);

            // remoteInitiatedRequestInboxQueue = initialSession.createTemporaryQueue();
            // String remoteInitiatedRequestInboxQueueName = remoteInitiatedRequestInboxQueue.getQueueName();

            RequestResponseQueuesManager tempQueueManager = new RequestResponseQueuesManager();
            String clientQueueInfo = tempQueueManager.initClientSide(initialSession);

            ownHandshakeInformation.setTemporaryQueueInformation(clientQueueInfo);

            // create request message
            Message handshakeRequestMessage = JmsProtocolUtils.createHandshakeMessage(ownHandshakeInformation, initialSession);

            // perform handshake
            ObjectMessage handshakeResponseMessage =
                (ObjectMessage) performBlockingJmsRequestResponse(initialSession, handshakeRequestMessage, initialInbox,
                    INITIAL_HANDSHAKE_TIMEOUT_MSEC);

            // extract the response, expecting the protocol set in the local HandshakeInformation
            JMSHandshakeInformation remoteHandshakeInformation =
                JmsProtocolUtils.parseHandshakeMessage(handshakeResponseMessage, ownHandshakeInformation.getProtocolVersionString());

            failOnIncompatibleVersions(remoteHandshakeInformation.getProtocolVersionString(),
                ownHandshakeInformation.getProtocolVersionString());

            tempQueueManager.finishClientSide(remoteHandshakeInformation.getTemporaryQueueInformation());

            // associate outgoing channel with the id of "mirror" channel
            setAssociatedMirrorChannelId(remoteHandshakeInformation.getChannelId());

            // spawn incoming request listener
            // note: this listener is not part of the message channel, so it must be closed explicitly
            // TODO clarify ownership
            final String uniqueTaskId =
                StringUtils.format("B2C Request Inbox Consumer for channel %s @ %s", remoteHandshakeInformation.getChannelId(),
                    tempQueueManager.getB2CRequestQueue());
            ConcurrencyUtils.getAsyncTaskService().execute(
                new RequestInboxConsumer(tempQueueManager.getB2CRequestQueue(), connection, remoteInitiatedConnectionEndpointHandler),
                uniqueTaskId);

            String outgoingRequestQueueName = tempQueueManager.getC2BRequestQueue();
            String incomingResponseQueueName = tempQueueManager.getC2BResponseQueue();
            setupNonBlockingRequestSending(outgoingRequestQueueName, incomingResponseQueueName);

            return remoteHandshakeInformation;
        } finally {
            initialSession.close();
        }
    }

}
