/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.nifi.io.socket.multicast;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetSocketAddress;
import java.net.MulticastSocket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implements a listener for protocol messages sent over multicast. If a message
 * is of type MulticastProtocolMessage, then the underlying protocol message is
 * passed to the handler. If the receiving handler produces a message response,
 * then the message is wrapped with a MulticastProtocolMessage before being sent
 * to the originator.
 *
 */
public abstract class MulticastListener {

    // constants
    private static final int DEFAULT_SHUTDOWN_LISTENER_SECONDS = 5;
    private static final int DEFAULT_MAX_PACKET_SIZE_BYTES = 512;

    private static final Logger logger = new org.apache.nifi.logging.NiFiLog(LoggerFactory.getLogger(MulticastListener.class));

    // immutable members
    private final int numThreads;
    private final InetSocketAddress multicastAddress;
    private final MulticastConfiguration configuration;

    private volatile ExecutorService executorService;     // volatile to guarantee most current value is visible
    private volatile MulticastSocket multicastSocket;     // volatile to guarantee most current value is visible

    private int shutdownListenerSeconds = DEFAULT_SHUTDOWN_LISTENER_SECONDS;
    private int maxPacketSizeBytes = DEFAULT_MAX_PACKET_SIZE_BYTES;

    public MulticastListener(
            final int numThreads,
            final InetSocketAddress multicastAddress,
            final MulticastConfiguration configuration) {

        if (numThreads <= 0) {
            throw new IllegalArgumentException("Number of threads may not be less than or equal to zero.");
        } else if (multicastAddress == null) {
            throw new IllegalArgumentException("Multicast address may not be null.");
        } else if (multicastAddress.getAddress().isMulticastAddress() == false) {
            throw new IllegalArgumentException("Multicast group must be a Class D address.");
        } else if (configuration == null) {
            throw new IllegalArgumentException("Multicast configuration may not be null.");
        }

        this.numThreads = numThreads;
        this.multicastAddress = multicastAddress;
        this.configuration = configuration;
    }

    /**
     * Implements the action to perform when a new datagram is received. This
     * class must not close the multicast socket.
     *
     * @param multicastSocket socket
     * @param packet the datagram packet
     */
    public abstract void dispatchRequest(final MulticastSocket multicastSocket, final DatagramPacket packet);

    public void start() throws IOException {

        if (isRunning()) {
            return;
        }

        multicastSocket = MulticastUtils.createMulticastSocket(multicastAddress.getPort(), configuration);
        multicastSocket.joinGroup(multicastAddress.getAddress());

        executorService = Executors.newFixedThreadPool(numThreads);

        final ExecutorService runnableExecServiceRef = executorService;
        final MulticastSocket runnableMulticastSocketRef = multicastSocket;

        new Thread(new Runnable() {
            @Override
            public void run() {
                while (runnableExecServiceRef.isShutdown() == false) {
                    try {
                        final byte[] buf = new byte[maxPacketSizeBytes];
                        final DatagramPacket packet = new DatagramPacket(buf, maxPacketSizeBytes);
                        runnableMulticastSocketRef.receive(packet);
                        runnableExecServiceRef.execute(new Runnable() {
                            @Override
                            public void run() {
                                dispatchRequest(multicastSocket, packet);
                            }
                        });
                    } catch (final SocketException | SocketTimeoutException ste) {
                        /* ignore so that we can accept connections in approximately a non-blocking fashion */
                    } catch (final Exception e) {
                        logger.warn("Cluster protocol receiver encountered exception: " + e, e);
                    }
                }
            }
        }).start();
    }

    public boolean isRunning() {
        return (executorService != null && executorService.isShutdown() == false);
    }

    public void stop() throws IOException {

        if (isRunning() == false) {
            return;
        }

        // shutdown executor service
        try {
            if (getShutdownListenerSeconds() <= 0) {
                executorService.shutdownNow();
            } else {
                executorService.shutdown();
            }
            executorService.awaitTermination(getShutdownListenerSeconds(), TimeUnit.SECONDS);
        } catch (final InterruptedException ex) {
            Thread.currentThread().interrupt();
        } finally {
            if (executorService.isTerminated()) {
                logger.info("Multicast Listener has been terminated successfully.");
            } else {
                logger.warn("Multicast Listener has not terminated properly.  There exists an uninterruptable thread that will take an indeterminate amount of time to stop.");
            }
        }

        // shutdown server socket
        if (multicastSocket.isClosed() == false) {
            multicastSocket.leaveGroup(multicastAddress.getAddress());
            multicastSocket.close();
        }

    }

    public int getShutdownListenerSeconds() {
        return shutdownListenerSeconds;
    }

    public void setShutdownListenerSeconds(final int shutdownListenerSeconds) {
        this.shutdownListenerSeconds = shutdownListenerSeconds;
    }

    public int getMaxPacketSizeBytes() {
        return maxPacketSizeBytes;
    }

    public void setMaxPacketSizeBytes(int maxPacketSizeBytes) {
        if (maxPacketSizeBytes <= 0) {
            throw new IllegalArgumentException("Max packet size must be greater than zero bytes.");
        }
        this.maxPacketSizeBytes = maxPacketSizeBytes;
    }

    public MulticastConfiguration getConfiguration() {
        return configuration;
    }

    public InetSocketAddress getMulticastAddress() {
        return multicastAddress;
    }

    public int getNumThreads() {
        return numThreads;
    }

}
